From c3d788ff4a2765b8ffc93938758a3369e63c76f5 Mon Sep 17 00:00:00 2001 From: Gerard Guillemas Martos Date: Tue, 13 Aug 2024 18:38:17 +0200 Subject: [PATCH] Add `BEARER` access type and its basic grant management (#4302) Co-authored-by: Emmanuel Keller Co-authored-by: Micha de Vries --- Cargo.lock | 5 +- core/Cargo.toml | 1 + core/src/cnf/mod.rs | 9 + core/src/dbs/statement.rs | 11 + core/src/err/mod.rs | 60 +- core/src/iam/entities/action.rs | 1 + core/src/iam/entities/resources/resource.rs | 2 + core/src/iam/entities/schema.rs | 5 +- core/src/iam/issue.rs | 2 +- core/src/iam/signin.rs | 2517 ++++++++++++++++++- core/src/iam/signup.rs | 14 +- core/src/iam/token.rs | 84 +- core/src/iam/verify.rs | 101 +- core/src/key/category.rs | 24 +- core/src/key/database/{ => access}/ac.rs | 8 +- core/src/key/database/access/all.rs | 60 + core/src/key/database/access/gr.rs | 93 + core/src/key/database/access/mod.rs | 3 + core/src/key/database/mod.rs | 2 +- core/src/key/mod.rs | 12 +- core/src/key/namespace/{ => access}/ac.rs | 19 +- core/src/key/namespace/access/all.rs | 55 + core/src/key/namespace/access/gr.rs | 88 + core/src/key/namespace/access/mod.rs | 3 + core/src/key/namespace/mod.rs | 2 +- core/src/key/root/{ => access}/ac.rs | 6 +- core/src/key/root/access/all.rs | 50 + core/src/key/root/access/gr.rs | 83 + core/src/key/root/access/mod.rs | 3 + core/src/key/root/mod.rs | 2 +- core/src/kvs/cache.rs | 31 + core/src/kvs/ds.rs | 4 +- core/src/kvs/tx.rs | 180 +- core/src/sql/access_type.rs | 88 +- core/src/sql/statement.rs | 10 +- core/src/sql/statements/access.rs | 628 +++++ core/src/sql/statements/define/access.rs | 16 +- core/src/sql/statements/mod.rs | 4 + core/src/sql/statements/remove/access.rs | 16 +- core/src/sql/value/value.rs | 9 + core/src/syn/lexer/keywords.rs | 4 + core/src/syn/parser/stmt/define.rs | 20 + core/src/syn/parser/stmt/mod.rs | 86 +- core/src/syn/parser/test/stmt.rs | 44 +- core/src/syn/token/keyword.rs | 4 + lib/tests/access.rs | 629 +++++ 46 files changed, 4928 insertions(+), 170 deletions(-) rename core/src/key/database/{ => access}/ac.rs (85%) create mode 100644 core/src/key/database/access/all.rs create mode 100644 core/src/key/database/access/gr.rs create mode 100644 core/src/key/database/access/mod.rs rename core/src/key/namespace/{ => access}/ac.rs (73%) create mode 100644 core/src/key/namespace/access/all.rs create mode 100644 core/src/key/namespace/access/gr.rs create mode 100644 core/src/key/namespace/access/mod.rs rename core/src/key/root/{ => access}/ac.rs (87%) create mode 100644 core/src/key/root/access/all.rs create mode 100644 core/src/key/root/access/gr.rs create mode 100644 core/src/key/root/access/mod.rs create mode 100644 core/src/sql/statements/access.rs create mode 100644 lib/tests/access.rs diff --git a/Cargo.lock b/Cargo.lock index 845549e4..566360bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5773,9 +5773,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "surreal" @@ -5980,6 +5980,7 @@ dependencies = [ "sha2", "snap", "storekey", + "subtle", "surrealdb-derive", "surrealdb-tikv-client", "surrealkv", diff --git a/core/Cargo.toml b/core/Cargo.toml index be3c4c61..e1aee137 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -142,6 +142,7 @@ sha1 = "0.10.6" sha2 = "0.10.8" snap = "1.1.0" storekey = "0.5.0" +subtle = "2.6" surrealkv = { version = "0.3.2", optional = true } surrealml = { version = "0.1.1", optional = true, package = "surrealml-core" } tempfile = { version = "3.10.1", optional = true } diff --git a/core/src/cnf/mod.rs b/core/src/cnf/mod.rs index 90d33c48..4a7be646 100644 --- a/core/src/cnf/mod.rs +++ b/core/src/cnf/mod.rs @@ -50,3 +50,12 @@ pub static INSECURE_FORWARD_ACCESS_ERRORS: Lazy = /// If the environment variable is not present or cannot be parsed, a default value of 50,000 is used. pub static EXTERNAL_SORTING_BUFFER_LIMIT: Lazy = lazy_env_parse!("SURREAL_EXTERNAL_SORTING_BUFFER_LIMIT", usize, 50_000); + +/// Enable experimental bearer access and stateful access grant management. Still under active development. +/// Using this experimental feature may introduce risks related to breaking changes and security issues. +#[cfg(not(test))] +pub static EXPERIMENTAL_BEARER_ACCESS: Lazy = + lazy_env_parse!("SURREAL_EXPERIMENTAL_BEARER_ACCESS", bool, false); +// Run tests with bearer access enabled as it introduces new functionality that needs to be tested. +#[cfg(test)] +pub static EXPERIMENTAL_BEARER_ACCESS: Lazy = Lazy::new(|| true); diff --git a/core/src/dbs/statement.rs b/core/src/dbs/statement.rs index 63ebd3f2..d421f536 100644 --- a/core/src/dbs/statement.rs +++ b/core/src/dbs/statement.rs @@ -9,6 +9,7 @@ use crate::sql::order::Orders; use crate::sql::output::Output; use crate::sql::split::Splits; use crate::sql::start::Start; +use crate::sql::statements::access::AccessStatement; use crate::sql::statements::create::CreateStatement; use crate::sql::statements::delete::DeleteStatement; use crate::sql::statements::insert::InsertStatement; @@ -32,6 +33,9 @@ pub(crate) enum Statement<'a> { Relate(&'a RelateStatement), Delete(&'a DeleteStatement), Insert(&'a InsertStatement), + // TODO(gguillemas): Document once bearer access is no longer experimental. + #[doc(hidden)] + Access(&'a AccessStatement), } impl<'a> From<&'a LiveStatement> for Statement<'a> { @@ -88,6 +92,12 @@ impl<'a> From<&'a InsertStatement> for Statement<'a> { } } +impl<'a> From<&'a AccessStatement> for Statement<'a> { + fn from(v: &'a AccessStatement) -> Self { + Statement::Access(v) + } +} + impl<'a> fmt::Display for Statement<'a> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -100,6 +110,7 @@ impl<'a> fmt::Display for Statement<'a> { Statement::Relate(v) => write!(f, "{v}"), Statement::Delete(v) => write!(f, "{v}"), Statement::Insert(v) => write!(f, "{v}"), + Statement::Access(v) => write!(f, "{v}"), } } } diff --git a/core/src/err/mod.rs b/core/src/err/mod.rs index c9eb1901..d98c4df8 100644 --- a/core/src/err/mod.rs +++ b/core/src/err/mod.rs @@ -932,43 +932,67 @@ pub enum Error { Serialization(String), /// The requested root access method already exists - #[error("The root access method '{value}' already exists")] + #[error("The root access method '{ac}' already exists")] AccessRootAlreadyExists { - value: String, + ac: String, }, /// The requested namespace access method already exists - #[error("The access method '{value}' already exists in the namespace '{ns}'")] + #[error("The access method '{ac}' already exists in the namespace '{ns}'")] AccessNsAlreadyExists { - value: String, + ac: String, ns: String, }, /// The requested database access method already exists - #[error("The access method '{value}' already exists in the database '{db}'")] + #[error("The access method '{ac}' already exists in the database '{db}'")] AccessDbAlreadyExists { - value: String, + ac: String, ns: String, db: String, }, /// The requested root access method does not exist - #[error("The root access method '{value}' does not exist")] + #[error("The root access method '{ac}' does not exist")] AccessRootNotFound { - value: String, + ac: String, + }, + + /// The requested root access grant does not exist + #[error("The root access grant '{gr}' does not exist")] + AccessGrantRootNotFound { + ac: String, + gr: String, }, /// The requested namespace access method does not exist - #[error("The access method '{value}' does not exist in the namespace '{ns}'")] + #[error("The access method '{ac}' does not exist in the namespace '{ns}'")] AccessNsNotFound { - value: String, + ac: String, + ns: String, + }, + + /// The requested namespace access grant does not exist + #[error("The access grant '{gr}' does not exist in the namespace '{ns}'")] + AccessGrantNsNotFound { + ac: String, + gr: String, ns: String, }, /// The requested database access method does not exist - #[error("The access method '{value}' does not exist in the database '{db}'")] + #[error("The access method '{ac}' does not exist in the database '{db}'")] AccessDbNotFound { - value: String, + ac: String, + ns: String, + db: String, + }, + + /// The requested database access grant does not exist + #[error("The access grant '{gr}' does not exist in the database '{db}'")] + AccessGrantDbNotFound { + ac: String, + gr: String, ns: String, db: String, }, @@ -1001,6 +1025,18 @@ pub enum Error { #[error("This record access method does not allow signin")] AccessRecordNoSignin, + #[error("This bearer access method requires a key to be provided")] + AccessBearerMissingKey, + + #[error("This bearer access grant has an invalid format")] + AccessGrantBearerInvalid, + + #[error("This access grant has an invalid subject")] + AccessGrantInvalidSubject, + + #[error("This access grant has been revoked")] + AccessGrantRevoked, + /// Found a table name for the record but this is not a valid table #[error("Found {value} for the Record ID but this is not a valid table name")] TbInvalid { diff --git a/core/src/iam/entities/action.rs b/core/src/iam/entities/action.rs index b4569d1e..b1340dac 100644 --- a/core/src/iam/entities/action.rs +++ b/core/src/iam/entities/action.rs @@ -54,6 +54,7 @@ impl From<&Statement<'_>> for Action { Statement::Relate(_) => Action::Edit, Statement::Delete(_) => Action::Edit, Statement::Insert(_) => Action::Edit, + Statement::Access(_) => Action::Edit, } } } diff --git a/core/src/iam/entities/resources/resource.rs b/core/src/iam/entities/resources/resource.rs index d093002e..1864767a 100644 --- a/core/src/iam/entities/resources/resource.rs +++ b/core/src/iam/entities/resources/resource.rs @@ -29,6 +29,7 @@ pub enum ResourceKind { Event, Field, Index, + Access, // IAM Actor, @@ -51,6 +52,7 @@ impl std::fmt::Display for ResourceKind { ResourceKind::Event => write!(f, "Event"), ResourceKind::Field => write!(f, "Field"), ResourceKind::Index => write!(f, "Index"), + ResourceKind::Access => write!(f, "Access"), ResourceKind::Actor => write!(f, "Actor"), } } diff --git a/core/src/iam/entities/schema.rs b/core/src/iam/entities/schema.rs index 533324c8..db3c35aa 100644 --- a/core/src/iam/entities/schema.rs +++ b/core/src/iam/entities/schema.rs @@ -46,6 +46,7 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy = Lazy::new(|| { "Event": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Field": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Index": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, + "Access": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, // IAM resource types "Role": {}, @@ -65,14 +66,14 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy = Lazy::new(|| { "View": { "appliesTo": { "principalTypes": [ "Actor" ], - "resourceTypes": [ "Any", "Namespace", "Database", "Record", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Actor" ], + "resourceTypes": [ "Any", "Namespace", "Database", "Record", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Access", "Actor" ], }, }, "Edit": { "appliesTo": { "principalTypes": [ "Actor" ], - "resourceTypes": [ "Any", "Namespace", "Database", "Record", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Actor" ], + "resourceTypes": [ "Any", "Namespace", "Database", "Record", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Access", "Actor" ], }, }, }, diff --git a/core/src/iam/issue.rs b/core/src/iam/issue.rs index 866cb6e0..57967564 100644 --- a/core/src/iam/issue.rs +++ b/core/src/iam/issue.rs @@ -5,7 +5,7 @@ use chrono::Duration as ChronoDuration; use chrono::Utc; use jsonwebtoken::EncodingKey; -pub(crate) fn config(alg: Algorithm, key: String) -> Result { +pub(crate) fn config(alg: Algorithm, key: &str) -> Result { match alg { Algorithm::Hs256 => Ok(EncodingKey::from_secret(key.as_ref())), Algorithm::Hs384 => Ok(EncodingKey::from_secret(key.as_ref())), diff --git a/core/src/iam/signin.rs b/core/src/iam/signin.rs index 6bdf77f2..b7c77d76 100644 --- a/core/src/iam/signin.rs +++ b/core/src/iam/signin.rs @@ -1,18 +1,23 @@ -use super::verify::{verify_db_creds, verify_ns_creds, verify_root_creds}; -use super::{Actor, Level}; -use crate::cnf::{INSECURE_FORWARD_ACCESS_ERRORS, SERVER_NAME}; +use super::verify::{ + authenticate_generic, authenticate_record, verify_db_creds, verify_ns_creds, verify_root_creds, +}; +use super::{Actor, Level, Role}; +use crate::cnf::{EXPERIMENTAL_BEARER_ACCESS, INSECURE_FORWARD_ACCESS_ERRORS, SERVER_NAME}; use crate::dbs::Session; use crate::err::Error; use crate::iam::issue::{config, expiration}; use crate::iam::token::{Claims, HEADER}; use crate::iam::Auth; use crate::kvs::{Datastore, LockType::*, TransactionType::*}; +use crate::sql::statements::{access, AccessGrant}; use crate::sql::AccessType; +use crate::sql::Datetime; use crate::sql::Object; use crate::sql::Value; use chrono::Utc; use jsonwebtoken::{encode, EncodingKey, Header}; use std::sync::Arc; +use subtle::ConstantTimeEq; use uuid::Uuid; pub async fn signin(kvs: &Datastore, session: &mut Session, vars: Object) -> Result { @@ -51,6 +56,14 @@ pub async fn signin(kvs: &Datastore, session: &mut Session, vars: Object) -> Res _ => Err(Error::MissingUserOrPass), } } + // NS signin with access method + (Some(ns), None, Some(ac)) => { + // Process the provided values + let ns = ns.to_raw_string(); + let ac = ac.to_raw_string(); + // Attempt to signin using specified access method + super::signin::ns_access(kvs, session, ns, ac, vars).await + } // NS signin with user credentials (Some(ns), None, None) => { // Get the provided user and pass @@ -113,14 +126,14 @@ pub async fn db_access( // All access method types are supported except for JWT // The JWT access method is the one that is internal to SurrealDB // The equivalent of signing in with JWT is to authenticate it - match av.kind.clone() { + match &av.kind { AccessType::Record(at) => { // Check if the record access method supports issuing tokens - let iss = match at.jwt.issue { - Some(iss) => iss, + let iss = match &at.jwt.issue { + Some(iss) => iss.clone(), _ => return Err(Error::AccessMethodMismatch), }; - match at.signin { + match &at.signin { // This record access allows signin Some(val) => { // Setup the query params @@ -137,7 +150,7 @@ pub async fn db_access( // There is a record returned Some(mut rid) => { // Create the authentication key - let key = config(iss.alg, iss.key)?; + let key = config(iss.alg, &iss.key)?; // Create the authentication claim let claims = Claims { iss: Some(SERVER_NAME.to_owned()), @@ -157,34 +170,21 @@ pub async fn db_access( let mut sess = Session::editor().with_ns(&ns).with_db(&db); sess.rd = Some(rid.clone().into()); - sess.tk = Some(claims.clone().into()); + sess.tk = Some((&claims).into()); sess.ip.clone_from(&session.ip); sess.or.clone_from(&session.or); - // Compute the value with the params - match kvs.evaluate(au.clone(), &sess, None).await { - Ok(val) => match val.record() { - Some(id) => { - // Update rid with result from AUTHENTICATE clause - rid = id; - } - _ => return Err(Error::InvalidAuth), - }, - Err(e) => return match e { - Error::Thrown(_) => Err(e), - e if *INSECURE_FORWARD_ACCESS_ERRORS => { - Err(e) - } - _ => Err(Error::InvalidAuth), - }, - } + rid = authenticate_record(kvs, &sess, au).await?; } // Log the authenticated access method info - trace!("Signing in with access method `{}`", ac); + trace!( + "Signing in to database with access method `{}`", + ac + ); // Create the authentication token let enc = encode(&Header::new(iss.alg.into()), &claims, &key); // Set the authentication on the session - session.tk = Some(claims.into()); + session.tk = Some((&claims).into()); session.ns = Some(ns.to_owned()); session.db = Some(db.to_owned()); session.ac = Some(ac.to_owned()); @@ -215,6 +215,120 @@ pub async fn db_access( _ => Err(Error::AccessRecordNoSignin), } } + AccessType::Bearer(at) => { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + if !*EXPERIMENTAL_BEARER_ACCESS { + // Return opaque error to avoid leaking the existence of the feature. + return Err(Error::InvalidAuth); + } + // Check if the bearer access method supports issuing tokens. + let iss = match &at.jwt.issue { + Some(iss) => iss.clone(), + _ => return Err(Error::AccessMethodMismatch), + }; + // Extract key identifier and key from the provided variables. + let (kid, key) = validate_grant_bearer(vars)?; + // Create a new readonly transaction + let tx = kvs.transaction(Read, Optimistic).await?; + // Fetch the specified access grant from storage + let gr = match tx.get_db_access_grant(&ns, &db, &ac, &kid).await { + Ok(gr) => gr, + // Return opaque error to avoid leaking existence of the grant. + _ => return Err(Error::InvalidAuth), + }; + // Ensure that the transaction is cancelled. + tx.cancel().await?; + // Authenticate bearer key against stored grant. + verify_grant_bearer(&gr, key)?; + // If the subject of the grant is a system user, get their roles. + let roles = if let Some(access::Subject::User(user)) = &gr.subject { + // Create a new readonly transaction. + let tx = kvs.transaction(Read, Optimistic).await?; + // Fetch the specified user from storage. + let user = tx.get_db_user(&ns, &db, user).await.map_err(|e| { + trace!("Error while authenticating to database `{ns}/{db}`: {e}"); + // Return opaque error to avoid leaking grant subject existence. + Error::InvalidAuth + })?; + // Ensure that the transaction is cancelled. + tx.cancel().await?; + user.roles.clone() + } else { + vec![] + }; + // Create the authentication key. + let key = config(iss.alg, &iss.key)?; + // Create the authentication claim. + let claims = Claims { + iss: Some(SERVER_NAME.to_owned()), + iat: Some(Utc::now().timestamp()), + nbf: Some(Utc::now().timestamp()), + exp: expiration(av.duration.token)?, + jti: Some(Uuid::new_v4().to_string()), + ns: Some(ns.to_owned()), + db: Some(db.to_owned()), + ac: Some(ac.to_owned()), + id: match &gr.subject { + Some(access::Subject::User(user)) => Some(user.to_raw()), + Some(access::Subject::Record(rid)) => Some(rid.to_raw()), + // Return opaque error as this code should not be reachable. + None => return Err(Error::InvalidAuth), + }, + roles: match &gr.subject { + Some(access::Subject::User(_)) => { + Some(roles.iter().map(|v| v.to_string()).collect()) + } + Some(access::Subject::Record(_)) => Default::default(), + // Return opaque error as this code should not be reachable. + None => return Err(Error::InvalidAuth), + }, + ..Claims::default() + }; + // AUTHENTICATE clause + if let Some(au) = &av.authenticate { + // Setup the system session for executing the clause + let mut sess = Session::editor().with_ns(&ns).with_db(&db); + sess.tk = Some((&claims).into()); + sess.ip.clone_from(&session.ip); + sess.or.clone_from(&session.or); + authenticate_generic(kvs, &sess, au).await?; + } + // Log the authenticated access method information. + trace!("Signing in to database with bearer access method `{}`", ac); + // Create the authentication token. + let enc = encode(&Header::new(iss.alg.into()), &claims, &key); + // Set the authentication on the session. + session.tk = Some((&claims).into()); + session.ns = Some(ns.to_owned()); + session.db = Some(db.to_owned()); + session.ac = Some(ac.to_owned()); + session.exp = expiration(av.duration.session)?; + match &gr.subject { + Some(access::Subject::User(user)) => { + session.au = Arc::new(Auth::new(Actor::new( + user.to_string(), + roles.iter().map(Role::from).collect(), + Level::Database(ns, db), + ))); + } + Some(access::Subject::Record(rid)) => { + session.au = Arc::new(Auth::new(Actor::new( + rid.to_string(), + Default::default(), + Level::Record(ns, db, rid.to_string()), + ))); + session.rd = Some(Value::from(rid.to_owned())); + } + // Return opaque error as this code should not be reachable. + None => return Err(Error::InvalidAuth), + }; + // Check the authentication token. + match enc { + // The authentication token was created successfully. + Ok(tk) => Ok(tk), + _ => Err(Error::TokenMakingFailed), + } + } _ => Err(Error::AccessMethodMismatch), } } @@ -251,7 +365,7 @@ pub async fn db_user( // Create the authentication token let enc = encode(&HEADER, &val, &key); // Set the authentication on the session - session.tk = Some(val.into()); + session.tk = Some((&val).into()); session.ns = Some(ns.to_owned()); session.db = Some(db.to_owned()); session.exp = expiration(u.duration.session)?; @@ -267,6 +381,133 @@ pub async fn db_user( } } +pub async fn ns_access( + kvs: &Datastore, + session: &mut Session, + ns: String, + ac: String, + vars: Object, +) -> Result { + // Create a new readonly transaction + let tx = kvs.transaction(Read, Optimistic).await?; + // Fetch the specified access method from storage + let access = tx.get_ns_access(&ns, &ac).await; + // Ensure that the transaction is cancelled + tx.cancel().await?; + // Check the provided access method exists + match access { + Ok(av) => { + // Check the access method type + match &av.kind { + AccessType::Bearer(at) => { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + if !*EXPERIMENTAL_BEARER_ACCESS { + // Return opaque error to avoid leaking the existence of the feature. + return Err(Error::InvalidAuth); + } + // Check if the bearer access method supports issuing tokens. + let iss = match &at.jwt.issue { + Some(iss) => iss.clone(), + _ => return Err(Error::AccessMethodMismatch), + }; + // Extract key identifier and key from the provided variables. + let (kid, key) = validate_grant_bearer(vars)?; + // Create a new readonly transaction + let tx = kvs.transaction(Read, Optimistic).await?; + // Fetch the specified access grant from storage + let gr = match tx.get_ns_access_grant(&ns, &ac, &kid).await { + Ok(gr) => gr, + // Return opaque error to avoid leaking existence of the grant. + _ => return Err(Error::InvalidAuth), + }; + // Ensure that the transaction is cancelled. + tx.cancel().await?; + // Authenticate bearer key against stored grant. + verify_grant_bearer(&gr, key)?; + // If the subject of the grant is a system user, get their roles. + let roles = if let Some(access::Subject::User(user)) = &gr.subject { + // Create a new readonly transaction. + let tx = kvs.transaction(Read, Optimistic).await?; + // Fetch the specified user from storage. + let user = tx.get_ns_user(&ns, user).await.map_err(|e| { + trace!("Error while authenticating to namespace `{ns}`: {e}"); + // Return opaque error to avoid leaking grant subject existence. + Error::InvalidAuth + })?; + // Ensure that the transaction is cancelled. + tx.cancel().await?; + user.roles.clone() + } else { + vec![] + }; + // Create the authentication key. + let key = config(iss.alg, &iss.key)?; + // Create the authentication claim. + let claims = Claims { + iss: Some(SERVER_NAME.to_owned()), + iat: Some(Utc::now().timestamp()), + nbf: Some(Utc::now().timestamp()), + exp: expiration(av.duration.token)?, + jti: Some(Uuid::new_v4().to_string()), + ns: Some(ns.to_owned()), + ac: Some(ac.to_owned()), + id: match &gr.subject { + Some(access::Subject::User(user)) => Some(user.to_raw()), + // Return opaque error as this code should not be reachable. + _ => return Err(Error::InvalidAuth), + }, + roles: match &gr.subject { + Some(access::Subject::User(_)) => { + Some(roles.iter().map(|v| v.to_string()).collect()) + } + // Return opaque error as this code should not be reachable. + _ => return Err(Error::InvalidAuth), + }, + ..Claims::default() + }; + // AUTHENTICATE clause + if let Some(au) = &av.authenticate { + // Setup the system session for executing the clause + let mut sess = Session::editor().with_ns(&ns); + sess.tk = Some((&claims).into()); + sess.ip.clone_from(&session.ip); + sess.or.clone_from(&session.or); + authenticate_generic(kvs, &sess, au).await?; + } + // Log the authenticated access method information. + trace!("Signing in to database with bearer access method `{}`", ac); + // Create the authentication token. + let enc = encode(&Header::new(iss.alg.into()), &claims, &key); + // Set the authentication on the session. + session.tk = Some((&claims).into()); + session.ns = Some(ns.to_owned()); + session.ac = Some(ac.to_owned()); + session.exp = expiration(av.duration.session)?; + match &gr.subject { + Some(access::Subject::User(user)) => { + session.au = Arc::new(Auth::new(Actor::new( + user.to_string(), + roles.iter().map(Role::from).collect(), + Level::Namespace(ns), + ))); + } + // Return opaque error as this code should not be reachable. + _ => return Err(Error::InvalidAuth), + }; + // Check the authentication token. + match enc { + // The authentication token was created successfully. + Ok(tk) => Ok(tk), + _ => Err(Error::TokenMakingFailed), + } + } + _ => Err(Error::AccessMethodMismatch), + } + } + _ => Err(Error::AccessNotFound), + } +} + pub async fn ns_user( kvs: &Datastore, session: &mut Session, @@ -294,7 +535,7 @@ pub async fn ns_user( // Create the authentication token let enc = encode(&HEADER, &val, &key); // Set the authentication on the session - session.tk = Some(val.into()); + session.tk = Some((&val).into()); session.ns = Some(ns.to_owned()); session.exp = expiration(u.duration.session)?; session.au = Arc::new((&u, Level::Namespace(ns.to_owned())).into()); @@ -350,6 +591,185 @@ pub async fn root_user( } } +pub async fn root_access( + kvs: &Datastore, + session: &mut Session, + ac: String, + vars: Object, +) -> Result { + // Create a new readonly transaction + let tx = kvs.transaction(Read, Optimistic).await?; + // Fetch the specified access method from storage + let access = tx.get_root_access(&ac).await; + // Ensure that the transaction is cancelled + tx.cancel().await?; + // Check the provided access method exists + match access { + Ok(av) => { + // Check the access method type + match &av.kind { + AccessType::Bearer(at) => { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + if !*EXPERIMENTAL_BEARER_ACCESS { + // Return opaque error to avoid leaking the existence of the feature. + return Err(Error::InvalidAuth); + } + // Check if the bearer access method supports issuing tokens. + let iss = match &at.jwt.issue { + Some(iss) => iss.clone(), + _ => return Err(Error::AccessMethodMismatch), + }; + // Extract key identifier and key from the provided variables. + let (kid, key) = validate_grant_bearer(vars)?; + // Create a new readonly transaction + let tx = kvs.transaction(Read, Optimistic).await?; + // Fetch the specified access grant from storage + let gr = match tx.get_root_access_grant(&ac, &kid).await { + Ok(gr) => gr, + // Return opaque error to avoid leaking existence of the grant. + _ => return Err(Error::InvalidAuth), + }; + // Ensure that the transaction is cancelled. + tx.cancel().await?; + // Authenticate bearer key against stored grant. + verify_grant_bearer(&gr, key)?; + // If the subject of the grant is a system user, get their roles. + let roles = if let Some(access::Subject::User(user)) = &gr.subject { + // Create a new readonly transaction. + let tx = kvs.transaction(Read, Optimistic).await?; + // Fetch the specified user from storage. + let user = tx.get_root_user(user).await.map_err(|e| { + trace!("Error while authenticating to root: {e}"); + // Return opaque error to avoid leaking grant subject existence. + Error::InvalidAuth + })?; + // Ensure that the transaction is cancelled. + tx.cancel().await?; + user.roles.clone() + } else { + vec![] + }; + // Create the authentication key. + let key = config(iss.alg, &iss.key)?; + // Create the authentication claim. + let claims = Claims { + iss: Some(SERVER_NAME.to_owned()), + iat: Some(Utc::now().timestamp()), + nbf: Some(Utc::now().timestamp()), + exp: expiration(av.duration.token)?, + jti: Some(Uuid::new_v4().to_string()), + ac: Some(ac.to_owned()), + id: match &gr.subject { + Some(access::Subject::User(user)) => Some(user.to_raw()), + // Return opaque error as this code should not be reachable. + _ => return Err(Error::InvalidAuth), + }, + roles: match &gr.subject { + Some(access::Subject::User(_)) => { + Some(roles.iter().map(|v| v.to_string()).collect()) + } + // Return opaque error as this code should not be reachable. + _ => return Err(Error::InvalidAuth), + }, + ..Claims::default() + }; + // AUTHENTICATE clause + if let Some(au) = &av.authenticate { + // Setup the system session for executing the clause + let mut sess = Session::editor(); + sess.tk = Some((&claims).into()); + sess.ip.clone_from(&session.ip); + sess.or.clone_from(&session.or); + authenticate_generic(kvs, &sess, au).await?; + } + // Log the authenticated access method information. + trace!("Signing in to database with bearer access method `{}`", ac); + // Create the authentication token. + let enc = encode(&Header::new(iss.alg.into()), &claims, &key); + // Set the authentication on the session. + session.tk = Some(claims.into()); + session.ac = Some(ac.to_owned()); + session.exp = expiration(av.duration.session)?; + match &gr.subject { + Some(access::Subject::User(user)) => { + session.au = Arc::new(Auth::new(Actor::new( + user.to_string(), + roles.iter().map(Role::from).collect(), + Level::Root, + ))); + } + // Return opaque error as this code should not be reachable. + _ => return Err(Error::InvalidAuth), + }; + // Check the authentication token. + match enc { + // The authentication token was created successfully. + Ok(tk) => Ok(tk), + _ => Err(Error::TokenMakingFailed), + } + } + _ => Err(Error::AccessMethodMismatch), + } + } + _ => Err(Error::AccessNotFound), + } +} + +pub fn validate_grant_bearer(vars: Object) -> Result<(String, String), Error> { + // Extract the provided key. + let key = match vars.get("key") { + Some(key) => key.to_raw_string(), + None => return Err(Error::AccessBearerMissingKey), + }; + if key.len() != access::GRANT_BEARER_LENGTH { + return Err(Error::AccessGrantBearerInvalid); + } + // Retrieve the prefix from the provided key. + let prefix: String = key.chars().take(access::GRANT_BEARER_PREFIX.len()).collect(); + // Check the length of the key prefix. + if prefix != access::GRANT_BEARER_PREFIX { + return Err(Error::AccessGrantBearerInvalid); + } + // Retrieve the key identifier from the provided key. + let kid: String = key + .chars() + .skip(access::GRANT_BEARER_PREFIX.len() + 1) + .take(access::GRANT_BEARER_ID_LENGTH) + .collect(); + // Check the length of the key identifier. + if kid.len() != access::GRANT_BEARER_ID_LENGTH { + return Err(Error::AccessGrantBearerInvalid); + }; + + Ok((kid, key)) +} + +pub fn verify_grant_bearer(gr: &Arc, key: String) -> Result<(), Error> { + // Check if the grant is revoked or expired. + match (&gr.expiration, &gr.revocation) { + (None, None) => {} + (Some(exp), None) => { + if exp < &Datetime::default() { + // Return opaque error to avoid leaking revocation status. + return Err(Error::InvalidAuth); + } + } + _ => return Err(Error::InvalidAuth), + } + // Check if the provided key matches the bearer key in the grant. + // We use time-constant comparison to prevent timing attacks. + if let access::Grant::Bearer(grant) = &gr.grant { + let grant_key_bytes: &[u8] = grant.key.as_bytes(); + let signin_key_bytes: &[u8] = key.as_bytes(); + let ok: bool = grant_key_bytes.ct_eq(signin_key_bytes).into(); + if !ok { + return Err(Error::InvalidAuth); + } + }; + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -1472,4 +1892,2041 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ== } } } + + #[tokio::test] + async fn test_signin_bearer_for_user_db() { + // Test with correct bearer key + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON DATABASE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + db: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = db_access( + &ds, + &mut sess, + "test".to_string(), + "test".to_string(), + "api".to_string(), + vars.into(), + ) + .await; + + assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); + assert_eq!(sess.ns, Some("test".to_string())); + assert_eq!(sess.db, Some("test".to_string())); + assert!(sess.au.is_db()); + assert_eq!(sess.au.level().ns(), Some("test")); + assert_eq!(sess.au.level().db(), Some("test")); + // Record users should not have roles + assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); + assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role"); + assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); + // Expiration should match the defined duration + let exp = sess.exp.unwrap(); + // Expiration should match the current time plus session duration with some margin + let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp(); + let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to follow the defined duration" + ); + } + + // Test with correct bearer key and AUTHENTICATE clause succeeding + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON DATABASE TYPE BEARER + AUTHENTICATE {{ + RETURN NONE + }} + DURATION FOR SESSION 2h + ; + DEFINE USER tobie ON DATABASE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + db: Some("test".to_string()), + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = db_access( + &ds, + &mut sess, + "test".to_string(), + "test".to_string(), + "api".to_string(), + vars.into(), + ) + .await; + + assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); + assert_eq!(sess.ns, Some("test".to_string())); + assert_eq!(sess.db, Some("test".to_string())); + assert!(sess.au.is_db()); + assert_eq!(sess.au.level().ns(), Some("test")); + assert_eq!(sess.au.level().db(), Some("test")); + // Record users should not have roles + assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); + assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role"); + assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); + // Expiration should match the defined duration + let exp = sess.exp.unwrap(); + // Expiration should match the current time plus session duration with some margin + let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp(); + let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to follow the defined duration" + ); + } + + // Test with correct bearer key and AUTHENTICATE clause failing + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON DATABASE TYPE BEARER + AUTHENTICATE {{ + THROW "Test authentication error"; + }} + DURATION FOR SESSION 2h + ; + DEFINE USER tobie ON DATABASE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + db: Some("test".to_string()), + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = db_access( + &ds, + &mut sess, + "test".to_string(), + "test".to_string(), + "api".to_string(), + vars.into(), + ) + .await; + + match res { + Err(Error::Thrown(e)) => { + assert_eq!(e, "Test authentication error") + } + res => panic!( + "Expected a thrown authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with expired grant + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON DATABASE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Wait for the grant to expire + std::thread::sleep(Duration::seconds(2).to_std().unwrap()); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + db: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = db_access( + &ds, + &mut sess, + "test".to_string(), + "test".to_string(), + "api".to_string(), + vars.into(), + ) + .await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with revoked grant + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON DATABASE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Get grant identifier from key + let kid = key.split("-").collect::>()[2]; + + // Revoke grant + ds.execute( + &format!( + r#" + ACCESS api REVOKE `{kid}`; + "# + ), + &sess, + None, + ) + .await + .unwrap(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + db: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = db_access( + &ds, + &mut sess, + "test".to_string(), + "test".to_string(), + "api".to_string(), + vars.into(), + ) + .await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with removed access method + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON DATABASE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Remove bearer access method + ds.execute(&format!(r#"REMOVE ACCESS api ON DATABASE;"#), &sess, None).await.unwrap(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + db: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = db_access( + &ds, + &mut sess, + "test".to_string(), + "test".to_string(), + "api".to_string(), + vars.into(), + ) + .await; + + match res { + Err(Error::AccessNotFound) => {} // ok + res => panic!( + "Expected an access method not found error, but instead received: {:?}", + res + ), + } + } + + // Test with missing key + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON DATABASE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let _key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + db: Some("test".to_string()), + ..Default::default() + }; + + // The key parameter is not inserted: + let vars: HashMap<&str, Value> = HashMap::new(); + // vars.insert("key", key.into()); + + let res = db_access( + &ds, + &mut sess, + "test".to_string(), + "test".to_string(), + "api".to_string(), + vars.into(), + ) + .await; + + match res { + Err(Error::AccessBearerMissingKey) => {} // ok + res => panic!( + "Expected a missing key authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key prefix part + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON DATABASE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Replace a character from the key prefix + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key[access::GRANT_BEARER_PREFIX.len() - 2] = '_'; + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + db: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = db_access( + &ds, + &mut sess, + "test".to_string(), + "test".to_string(), + "api".to_string(), + vars.into(), + ) + .await; + + match res { + Err(Error::AccessGrantBearerInvalid) => {} // ok + res => panic!( + "Expected an invalid key authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key length + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON DATABASE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Remove a character from the bearer key + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key.truncate(access::GRANT_BEARER_LENGTH - 1); + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + db: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = db_access( + &ds, + &mut sess, + "test".to_string(), + "test".to_string(), + "api".to_string(), + vars.into(), + ) + .await; + + match res { + Err(Error::AccessGrantBearerInvalid) => {} // ok + res => panic!( + "Expected an invalid key authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key identifier part + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON DATABASE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Replace a character from the key identifier + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key[access::GRANT_BEARER_PREFIX.len() + 2] = '_'; + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + db: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = db_access( + &ds, + &mut sess, + "test".to_string(), + "test".to_string(), + "api".to_string(), + vars.into(), + ) + .await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key value + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test").with_db("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON DATABASE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Replace a character from the key value + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key + [access::GRANT_BEARER_PREFIX.len() + 1 + access::GRANT_BEARER_ID_LENGTH + 2] = '_'; + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + db: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = db_access( + &ds, + &mut sess, + "test".to_string(), + "test".to_string(), + "api".to_string(), + vars.into(), + ) + .await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + } + + #[tokio::test] + async fn test_signin_bearer_for_user_ns() { + // Test with correct bearer key + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON NAMESPACE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = + ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; + + assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); + assert_eq!(sess.ns, Some("test".to_string())); + assert_eq!(sess.db, None); + assert!(sess.au.is_ns()); + assert_eq!(sess.au.level().ns(), Some("test")); + assert_eq!(sess.au.level().db(), None); + // Record users should not have roles + assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); + assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role"); + assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); + // Expiration should match the defined duration + let exp = sess.exp.unwrap(); + // Expiration should match the current time plus session duration with some margin + let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp(); + let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to follow the defined duration" + ); + } + + // Test with correct bearer key and AUTHENTICATE clause succeeding + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON NAMESPACE TYPE BEARER + AUTHENTICATE {{ + RETURN NONE + }} + DURATION FOR SESSION 2h + ; + DEFINE USER tobie ON NAMESPACE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = + ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; + + assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); + assert_eq!(sess.ns, Some("test".to_string())); + assert_eq!(sess.db, None); + assert!(sess.au.is_ns()); + assert_eq!(sess.au.level().ns(), Some("test")); + assert_eq!(sess.au.level().db(), None); + // Record users should not have roles + assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); + assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role"); + assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); + // Expiration should match the defined duration + let exp = sess.exp.unwrap(); + // Expiration should match the current time plus session duration with some margin + let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp(); + let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to follow the defined duration" + ); + } + + // Test with correct bearer key and AUTHENTICATE clause failing + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON NAMESPACE TYPE BEARER + AUTHENTICATE {{ + THROW "Test authentication error"; + }} + DURATION FOR SESSION 2h + ; + DEFINE USER tobie ON NAMESPACE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = + ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; + + match res { + Err(Error::Thrown(e)) => { + assert_eq!(e, "Test authentication error") + } + res => panic!( + "Expected a thrown authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with expired grant + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON NAMESPACE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Wait for the grant to expire + std::thread::sleep(Duration::seconds(2).to_std().unwrap()); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = + ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with revoked grant + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON NAMESPACE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Get grant identifier from key + let kid = key.split("-").collect::>()[2]; + + // Revoke grant + ds.execute( + &format!( + r#" + ACCESS api REVOKE `{kid}`; + "# + ), + &sess, + None, + ) + .await + .unwrap(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = + ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with removed access method + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON NAMESPACE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Remove bearer access method + ds.execute(&format!(r#"REMOVE ACCESS api ON NAMESPACE;"#), &sess, None).await.unwrap(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = + ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; + + match res { + Err(Error::AccessNotFound) => {} // ok + res => panic!( + "Expected an access method not found error, but instead received: {:?}", + res + ), + } + } + + // Test with missing key + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON NAMESPACE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let _key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + ..Default::default() + }; + + // The key parameter is not inserted: + let vars: HashMap<&str, Value> = HashMap::new(); + // vars.insert("key", key.into()); + + let res = + ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; + + match res { + Err(Error::AccessBearerMissingKey) => {} // ok + res => panic!( + "Expected a missing key authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key prefix part + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON NAMESPACE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Replace a character from the key prefix + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key[access::GRANT_BEARER_PREFIX.len() - 2] = '_'; + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = + ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; + + match res { + Err(Error::AccessGrantBearerInvalid) => {} // ok + res => panic!( + "Expected an invalid key authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key length + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON NAMESPACE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Remove a character from the bearer key + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key.truncate(access::GRANT_BEARER_LENGTH - 1); + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = + ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; + + match res { + Err(Error::AccessGrantBearerInvalid) => {} // ok + res => panic!( + "Expected an invalid key authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key identifier part + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON NAMESPACE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Replace a character from the key identifier + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key[access::GRANT_BEARER_PREFIX.len() + 2] = '_'; + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = + ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key value + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner().with_ns("test"); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON NAMESPACE ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Replace a character from the key value + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key + [access::GRANT_BEARER_PREFIX.len() + 1 + access::GRANT_BEARER_ID_LENGTH + 2] = '_'; + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ns: Some("test".to_string()), + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = + ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + } + + #[tokio::test] + async fn test_signin_bearer_for_user_root() { + // Test with correct bearer key + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON ROOT ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + + assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); + assert_eq!(sess.ns, None); + assert_eq!(sess.db, None); + assert!(sess.au.is_root()); + assert_eq!(sess.au.level().ns(), None); + assert_eq!(sess.au.level().db(), None); + // Record users should not have roles + assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); + assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role"); + assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); + // Expiration should match the defined duration + let exp = sess.exp.unwrap(); + // Expiration should match the current time plus session duration with some margin + let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp(); + let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to follow the defined duration" + ); + } + + // Test with correct bearer key and AUTHENTICATE clause succeeding + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON ROOT TYPE BEARER + AUTHENTICATE {{ + RETURN NONE + }} + DURATION FOR SESSION 2h + ; + DEFINE USER tobie ON ROOT ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + + assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res); + assert_eq!(sess.ns, None); + assert_eq!(sess.db, None); + assert!(sess.au.is_root()); + assert_eq!(sess.au.level().ns(), None); + assert_eq!(sess.au.level().db(), None); + // Record users should not have roles + assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); + assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role"); + assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); + // Expiration should match the defined duration + let exp = sess.exp.unwrap(); + // Expiration should match the current time plus session duration with some margin + let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp(); + let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp(); + assert!( + exp > min_exp && exp < max_exp, + "Session expiration is expected to follow the defined duration" + ); + } + + // Test with correct bearer key and AUTHENTICATE clause failing + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON ROOT TYPE BEARER + AUTHENTICATE {{ + THROW "Test authentication error"; + }} + DURATION FOR SESSION 2h + ; + DEFINE USER tobie ON ROOT ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + + match res { + Err(Error::Thrown(e)) => { + assert_eq!(e, "Test authentication error") + } + res => panic!( + "Expected a thrown authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with expired grant + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON ROOT ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Wait for the grant to expire + std::thread::sleep(Duration::seconds(2).to_std().unwrap()); + + // Signin with the bearer key + let mut sess = Session { + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with revoked grant + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON ROOT ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Get grant identifier from key + let kid = key.split("-").collect::>()[2]; + + // Revoke grant + ds.execute( + &format!( + r#" + ACCESS api REVOKE `{kid}`; + "# + ), + &sess, + None, + ) + .await + .unwrap(); + + // Signin with the bearer key + let mut sess = Session { + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with removed access method + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h; + DEFINE USER tobie ON ROOT ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let key = grant.get("key").unwrap().clone().as_string(); + + // Remove bearer access method + ds.execute(&format!(r#"REMOVE ACCESS api ON ROOT;"#), &sess, None).await.unwrap(); + + // Signin with the bearer key + let mut sess = Session { + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + + match res { + Err(Error::AccessNotFound) => {} // ok + res => panic!( + "Expected an access method not found error, but instead received: {:?}", + res + ), + } + } + + // Test with missing key + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON ROOT ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let _key = grant.get("key").unwrap().clone().as_string(); + + // Signin with the bearer key + let mut sess = Session { + ..Default::default() + }; + + // The key parameter is not inserted: + let vars: HashMap<&str, Value> = HashMap::new(); + // vars.insert("key", key.into()); + + let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + + match res { + Err(Error::AccessBearerMissingKey) => {} // ok + res => panic!( + "Expected a missing key authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key prefix part + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON ROOT ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Replace a character from the key prefix + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key[access::GRANT_BEARER_PREFIX.len() - 2] = '_'; + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + + match res { + Err(Error::AccessGrantBearerInvalid) => {} // ok + res => panic!( + "Expected an invalid key authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key length + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON ROOT ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Remove a character from the bearer key + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key.truncate(access::GRANT_BEARER_LENGTH - 1); + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + + match res { + Err(Error::AccessGrantBearerInvalid) => {} // ok + res => panic!( + "Expected an invalid key authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key identifier part + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON ROOT ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Replace a character from the key identifier + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key[access::GRANT_BEARER_PREFIX.len() + 2] = '_'; + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + + // Test with incorrect bearer key value + { + let ds = Datastore::new("memory").await.unwrap(); + let sess = Session::owner(); + let res = ds + .execute( + r#" + DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h; + DEFINE USER tobie ON ROOT ROLES EDITOR; + ACCESS api GRANT FOR USER tobie; + "#, + &sess, + None, + ) + .await + .unwrap(); + + // Get the bearer key from grant. + let result = if let Ok(res) = &res.last().unwrap().result { + res.clone() + } else { + panic!("Unable to retrieve bearer key grant"); + }; + let grant = result + .coerce_to_object() + .unwrap() + .get("grant") + .unwrap() + .clone() + .coerce_to_object() + .unwrap(); + let valid_key = grant.get("key").unwrap().clone().as_string(); + + // Replace a character from the key value + let mut invalid_key: Vec = valid_key.chars().collect(); + invalid_key + [access::GRANT_BEARER_PREFIX.len() + 1 + access::GRANT_BEARER_ID_LENGTH + 2] = '_'; + let key: String = invalid_key.into_iter().collect(); + + // Signin with the bearer key + let mut sess = Session { + ..Default::default() + }; + let mut vars: HashMap<&str, Value> = HashMap::new(); + vars.insert("key", key.into()); + let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await; + + match res { + Err(Error::InvalidAuth) => {} // ok + res => panic!( + "Expected a generic authentication error, but instead received: {:?}", + res + ), + } + } + } } diff --git a/core/src/iam/signup.rs b/core/src/iam/signup.rs index 999a4632..b2d84e52 100644 --- a/core/src/iam/signup.rs +++ b/core/src/iam/signup.rs @@ -57,14 +57,14 @@ pub async fn db_access( Ok(av) => { // Check the access method type // Currently, only the record access method supports signup - match av.kind.clone() { + match &av.kind { AccessType::Record(at) => { // Check if the record access method supports issuing tokens - let iss = match at.jwt.issue { + let iss = match &at.jwt.issue { Some(iss) => iss, _ => return Err(Error::AccessMethodMismatch), }; - match at.signup { + match &at.signup { // This record access allows signup Some(val) => { // Setup the query params @@ -81,7 +81,7 @@ pub async fn db_access( // There is a record returned Some(mut rid) => { // Create the authentication key - let key = config(iss.alg, iss.key)?; + let key = config(iss.alg, &iss.key)?; // Create the authentication claim let claims = Claims { iss: Some(SERVER_NAME.to_owned()), @@ -101,11 +101,11 @@ pub async fn db_access( let mut sess = Session::editor().with_ns(&ns).with_db(&db); sess.rd = Some(rid.clone().into()); - sess.tk = Some(claims.clone().into()); + sess.tk = Some((&claims).into()); sess.ip.clone_from(&session.ip); sess.or.clone_from(&session.or); // Compute the value with the params - match kvs.evaluate(au.clone(), &sess, None).await { + match kvs.evaluate(au, &sess, None).await { Ok(val) => match val.record() { Some(id) => { // Update rid with result from AUTHENTICATE clause @@ -128,7 +128,7 @@ pub async fn db_access( let enc = encode(&Header::new(iss.alg.into()), &claims, &key); // Set the authentication on the session - session.tk = Some(claims.into()); + session.tk = Some((&claims).into()); session.ns = Some(ns.to_owned()); session.db = Some(db.to_owned()); session.ac = Some(ac.to_owned()); diff --git a/core/src/iam/token.rs b/core/src/iam/token.rs index 27aedc0f..37875d41 100644 --- a/core/src/iam/token.rs +++ b/core/src/iam/token.rs @@ -147,7 +147,89 @@ impl From for Value { continue; } }; - out.insert(claim, claim_value); + out.insert(claim.to_owned(), claim_value); + } + } + // Return value + out.into() + } +} + +impl From<&Claims> for Value { + fn from(v: &Claims) -> Value { + // Set default value + let mut out = Object::default(); + // Add iss field if set + if let Some(iss) = &v.iss { + out.insert("iss".to_string(), iss.clone().into()); + } + // Add sub field if set + if let Some(sub) = &v.sub { + out.insert("sub".to_string(), sub.clone().into()); + } + // Add aud field if set + if let Some(aud) = &v.aud { + match aud { + Audience::Single(v) => out.insert("aud".to_string(), v.clone().into()), + Audience::Multiple(v) => out.insert("aud".to_string(), v.clone().into()), + }; + } + // Add iat field if set + if let Some(iat) = v.iat { + out.insert("iat".to_string(), iat.into()); + } + // Add nbf field if set + if let Some(nbf) = v.nbf { + out.insert("nbf".to_string(), nbf.into()); + } + // Add exp field if set + if let Some(exp) = v.exp { + out.insert("exp".to_string(), exp.into()); + } + // Add jti field if set + if let Some(jti) = &v.jti { + out.insert("jti".to_string(), jti.clone().into()); + } + // Add NS field if set + if let Some(ns) = &v.ns { + out.insert("NS".to_string(), ns.clone().into()); + } + // Add DB field if set + if let Some(db) = &v.db { + out.insert("DB".to_string(), db.clone().into()); + } + // Add AC field if set + if let Some(ac) = &v.ac { + out.insert("AC".to_string(), ac.clone().into()); + } + // Add ID field if set + if let Some(id) = &v.id { + out.insert("ID".to_string(), id.clone().into()); + } + // Add RL field if set + if let Some(role) = &v.roles { + out.insert("RL".to_string(), role.clone().into()); + } + // Add custom claims if set + if let Some(custom_claims) = &v.custom_claims { + for (claim, value) in custom_claims { + // Serialize the raw JSON string representing the claim value + let claim_json = match serde_json::to_string(&value) { + Ok(claim_json) => claim_json, + Err(err) => { + debug!("Failed to serialize token claim '{}': {}", claim, err); + continue; + } + }; + // Parse that JSON string into the corresponding SurrealQL value + let claim_value = match json(&claim_json) { + Ok(claim_value) => claim_value, + Err(err) => { + debug!("Failed to parse token claim '{}': {}", claim, err); + continue; + } + }; + out.insert(claim.to_owned(), claim_value); } } // Return value diff --git a/core/src/iam/verify.rs b/core/src/iam/verify.rs index 5a98e315..8fde54ea 100644 --- a/core/src/iam/verify.rs +++ b/core/src/iam/verify.rs @@ -5,7 +5,7 @@ use crate::err::Error; use crate::iam::jwks; use crate::iam::{issue::expiration, token::Claims, Actor, Auth, Level, Role}; use crate::kvs::{Datastore, LockType::*, TransactionType::*}; -use crate::sql::access_type::{AccessType, JwtAccessVerify}; +use crate::sql::access_type::{AccessType, Jwt, JwtAccessVerify}; use crate::sql::{statements::DefineUserStatement, Algorithm, Thing, Value}; use crate::syn; use argon2::{Argon2, PasswordHash, PasswordVerifier}; @@ -130,7 +130,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul // Decode the token without verifying let token_data = decode::(token, &KEY, &DUD)?; // Convert the token to a SurrealQL object value - let value = token_data.claims.clone().into(); + let value = (&token_data.claims).into(); // Check if the auth token can be used if let Some(nbf) = token_data.claims.nbf { if nbf > Utc::now().timestamp() { @@ -146,7 +146,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul } } // Check the token authentication claims - match token_data.claims.clone() { + match &token_data.claims { // Check if this is record access Claims { ns: Some(ns), @@ -160,9 +160,9 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul // Create a new readonly transaction let tx = kvs.transaction(Read, Optimistic).await?; // Parse the record id - let mut rid = syn::thing(&id)?; + let mut rid = syn::thing(id)?; // Get the database access method - let de = tx.get_db_access(&ns, &db, &ac).await?; + let de = tx.get_db_access(ns, db, ac).await?; // Ensure that the transaction is cancelled tx.cancel().await?; // Obtain the configuration to verify the token based on the access method @@ -187,12 +187,12 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul // AUTHENTICATE clause if let Some(au) = &de.authenticate { // Setup the system session for finding the signin record - let mut sess = Session::editor().with_ns(&ns).with_db(&db); + let mut sess = Session::editor().with_ns(ns).with_db(db); sess.rd = Some(rid.clone().into()); - sess.tk = Some(token_data.claims.clone().into()); + sess.tk = Some((&token_data.claims).into()); sess.ip.clone_from(&session.ip); sess.or.clone_from(&session.or); - rid = authenticate_record(kvs, &sess, au.clone()).await?; + rid = authenticate_record(kvs, &sess, au).await?; } // Log the success debug!("Authenticated with record access method `{}`", ac); @@ -206,7 +206,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul session.au = Arc::new(Auth::new(Actor::new( rid.to_string(), Default::default(), - Level::Record(ns, db, rid.to_string()), + Level::Record(ns.to_string(), db.to_string(), rid.to_string()), ))); Ok(()) } @@ -223,14 +223,14 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul // Create a new readonly transaction let tx = kvs.transaction(Read, Optimistic).await?; // Get the database access method - let de = tx.get_db_access(&ns, &db, &ac).await?; + let de = tx.get_db_access(ns, db, ac).await?; // Ensure that the transaction is cancelled tx.cancel().await?; // Obtain the configuration to verify the token based on the access method match &de.kind { - // If the access type is Jwt, this is database access - AccessType::Jwt(at) => { - let cf = match &at.verify { + // If the access type is Jwt or Bearer, this is database access + AccessType::Jwt(_) | AccessType::Bearer(_) => { + let cf = match &de.kind.jwt().verify { JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()), #[cfg(feature = "jwks")] JwtAccessVerify::Jwks(jwks) => { @@ -247,15 +247,15 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul decode::(token, &cf.0, &cf.1)?; // AUTHENTICATE clause if let Some(au) = &de.authenticate { - // Setup the system session for finding the signin record - let mut sess = Session::editor().with_ns(&ns).with_db(&db); - sess.tk = Some(token_data.claims.clone().into()); + // Setup the system session for executing the clause + let mut sess = Session::editor().with_ns(ns).with_db(db); + sess.tk = Some((&token_data.claims).into()); sess.ip.clone_from(&session.ip); sess.or.clone_from(&session.or); - authenticate_jwt(kvs, &sess, au.clone()).await?; + authenticate_generic(kvs, &sess, au).await?; } // Parse the roles - let roles = match token_data.claims.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 @@ -277,7 +277,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul session.au = Arc::new(Auth::new(Actor::new( de.name.to_string(), roles, - Level::Database(ns, db), + Level::Database(ns.to_string(), db.to_string()), ))); } // If the access type is Record, this is record access @@ -304,11 +304,11 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul decode::(token, &cf.0, &cf.1)?; // AUTHENTICATE clause // Setup the system session for finding the signin record - let mut sess = Session::editor().with_ns(&ns).with_db(&db); - sess.tk = Some(token_data.claims.clone().into()); + let mut sess = Session::editor().with_ns(ns).with_db(db); + sess.tk = Some((&token_data.claims).into()); sess.ip.clone_from(&session.ip); sess.or.clone_from(&session.or); - let rid = authenticate_record(kvs, &sess, au.clone()).await?; + let rid = authenticate_record(kvs, &sess, au).await?; // Log the success debug!("Authenticated with record access method `{}`", ac); // Set the session @@ -321,7 +321,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul session.au = Arc::new(Auth::new(Actor::new( rid.to_string(), Default::default(), - Level::Record(ns, db, rid.to_string()), + Level::Record(ns.to_string(), db.to_string(), rid.to_string()), ))); } _ => return Err(Error::AccessMethodMismatch), @@ -341,7 +341,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul // Create a new readonly transaction let tx = kvs.transaction(Read, Optimistic).await?; // Get the database user - let de = tx.get_db_user(&ns, &db, &id).await.map_err(|e| { + let de = tx.get_db_user(ns, db, id).await.map_err(|e| { trace!("Error while authenticating to database `{db}`: {e}"); Error::InvalidAuth })?; @@ -361,7 +361,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul session.au = Arc::new(Auth::new(Actor::new( id.to_string(), de.roles.iter().map(|r| r.into()).collect(), - Level::Database(ns, db), + Level::Database(ns.to_string(), db.to_string()), ))); Ok(()) } @@ -376,12 +376,12 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul // Create a new readonly transaction let tx = kvs.transaction(Read, Optimistic).await?; // Get the namespace access method - let de = tx.get_ns_access(&ns, &ac).await?; + let de = tx.get_ns_access(ns, ac).await?; // Ensure that the transaction is cancelled tx.cancel().await?; // Obtain the configuration to verify the token based on the access method let cf = match &de.kind { - AccessType::Jwt(ac) => match &ac.verify { + AccessType::Jwt(_) | AccessType::Bearer(_) => match &de.kind.jwt().verify { JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()), #[cfg(feature = "jwks")] JwtAccessVerify::Jwks(jwks) => { @@ -400,15 +400,15 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul decode::(token, &cf.0, &cf.1)?; // AUTHENTICATE clause if let Some(au) = &de.authenticate { - // Setup the system session for finding the signin record - let mut sess = Session::editor().with_ns(&ns); - sess.tk = Some(token_data.claims.clone().into()); + // Setup the system session for executing the clause + let mut sess = Session::editor().with_ns(ns); + sess.tk = Some((&token_data.claims).into()); sess.ip.clone_from(&session.ip); sess.or.clone_from(&session.or); - authenticate_jwt(kvs, &sess, au.clone()).await?; + authenticate_generic(kvs, &sess, au).await?; } // Parse the roles - let roles = match token_data.claims.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 @@ -426,8 +426,11 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul session.ns = Some(ns.to_owned()); 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::Namespace(ns)))); + session.au = Arc::new(Auth::new(Actor::new( + de.name.to_string(), + roles, + Level::Namespace(ns.to_string()), + ))); Ok(()) } // Check if this is namespace authentication with user credentials @@ -441,7 +444,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul // Create a new readonly transaction let tx = kvs.transaction(Read, Optimistic).await?; // Get the namespace user - let de = tx.get_ns_user(&ns, &id).await.map_err(|e| { + let de = tx.get_ns_user(ns, id).await.map_err(|e| { trace!("Error while authenticating to namespace `{ns}`: {e}"); Error::InvalidAuth })?; @@ -460,7 +463,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul session.au = Arc::new(Auth::new(Actor::new( id.to_string(), de.roles.iter().map(|r| r.into()).collect(), - Level::Namespace(ns), + Level::Namespace(ns.to_string()), ))); Ok(()) } @@ -474,12 +477,12 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul // Create a new readonly transaction let tx = kvs.transaction(Read, Optimistic).await?; // Get the namespace access method - let de = tx.get_root_access(&ac).await?; + let de = tx.get_root_access(ac).await?; // Ensure that the transaction is cancelled tx.cancel().await?; // Obtain the configuration to verify the token based on the access method let cf = match &de.kind { - AccessType::Jwt(ac) => match &ac.verify { + AccessType::Jwt(_) | AccessType::Bearer(_) => match &de.kind.jwt().verify { JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()), #[cfg(feature = "jwks")] JwtAccessVerify::Jwks(jwks) => { @@ -498,15 +501,15 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul decode::(token, &cf.0, &cf.1)?; // AUTHENTICATE clause if let Some(au) = &de.authenticate { - // Setup the system session for finding the signin record + // Setup the system session for executing the clause let mut sess = Session::editor(); - sess.tk = Some(token_data.claims.clone().into()); + sess.tk = Some((&token_data.claims).into()); sess.ip.clone_from(&session.ip); sess.or.clone_from(&session.or); - authenticate_jwt(kvs, &sess, au.clone()).await?; + authenticate_generic(kvs, &sess, au).await?; } // Parse the roles - let roles = match token_data.claims.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 @@ -536,7 +539,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul // Create a new readonly transaction let tx = kvs.transaction(Read, Optimistic).await?; // Get the namespace user - let de = tx.get_root_user(&id).await.map_err(|e| { + let de = tx.get_root_user(id).await.map_err(|e| { trace!("Error while authenticating to root: {e}"); Error::InvalidAuth })?; @@ -642,11 +645,11 @@ fn verify_pass(pass: &str, hash: &str) -> Result<(), Error> { } } -// Execute the AUTHENTICATE clause for a record access method -async fn authenticate_record( +// Execute the AUTHENTICATE clause for a Record access method +pub async fn authenticate_record( kvs: &Datastore, session: &Session, - authenticate: Value, + authenticate: &Value, ) -> Result { match kvs.evaluate(authenticate, session, None).await { Ok(val) => match val.record() { @@ -664,11 +667,11 @@ async fn authenticate_record( } } -// Execute the AUTHENTICATE clause for a JWT access method -async fn authenticate_jwt( +// Execute the AUTHENTICATE clause for any other access method +pub async fn authenticate_generic( kvs: &Datastore, session: &Session, - authenticate: Value, + authenticate: &Value, ) -> Result<(), Error> { match kvs.evaluate(authenticate, session, None).await { Ok(val) => { diff --git a/core/src/key/category.rs b/core/src/key/category.rs index fda1100f..4a3975e0 100644 --- a/core/src/key/category.rs +++ b/core/src/key/category.rs @@ -12,8 +12,12 @@ pub(crate) trait Categorise { pub enum Category { /// crate::key::root::all / Root, - /// crate::key::root::ac /!ac{ac} + /// crate::key::root::access::ac /!ac{ac} Access, + /// crate::key::root::access::all /*{ac} + AccessRoot, + /// crate::key::root::access::gr /*{ac}!gr{gr} + AccessGrant, /// crate::key::root::nd /!nd{nd} Node, /// crate::key::root::ni /!ni @@ -43,8 +47,12 @@ pub enum Category { NamespaceRoot, /// crate::key::namespace::db /*{ns}!db{db} DatabaseAlias, - /// crate::key::namespace::ac /*{ns}!ac{ac} + /// crate::key::namespace::access::ac /*{ns}!ac{ac} NamespaceAccess, + /// crate::key::namespace::access::all /*{ns}*{ac} + NamespaceAccessRoot, + /// crate::key::namespace::access::gr /*{ns}*{ac}!gr{gr} + NamespaceAccessGrant, /// crate::key::namespace::us /*{ns}!us{us} NamespaceUser, /// @@ -52,8 +60,12 @@ pub enum Category { /// /// crate::key::database::all /*{ns}*{db} DatabaseRoot, - /// crate::key::database::ac /*{ns}*{db}!ac{ac} + /// crate::key::database::access::ac /*{ns}*{db}!ac{ac} DatabaseAccess, + /// crate::key::database::access::all /*{ns}*{db}*{ac} + DatabaseAccessRoot, + /// crate::key::database::access::gr /*{ns}*{db}*ac!gr{gr} + DatabaseAccessGrant, /// crate::key::database::az /*{ns}*{db}!az{az} DatabaseAnalyzer, /// crate::key::database::fc /*{ns}*{db}!fn{fc} @@ -136,6 +148,8 @@ impl Display for Category { let name = match self { Self::Root => "Root", Self::Access => "Access", + Self::AccessRoot => "AccessRoot", + Self::AccessGrant => "AccessGrant", Self::Node => "Node", Self::NamespaceIdentifier => "NamespaceIdentifier", Self::Namespace => "Namespace", @@ -146,9 +160,13 @@ impl Display for Category { Self::DatabaseAlias => "DatabaseAlias", Self::DatabaseIdentifier => "DatabaseIdentifier", Self::NamespaceAccess => "NamespaceAccess", + Self::NamespaceAccessRoot => "NamespaceAccessRoot", + Self::NamespaceAccessGrant => "NamespaceAccessGrant", Self::NamespaceUser => "NamespaceUser", Self::DatabaseRoot => "DatabaseRoot", Self::DatabaseAccess => "DatabaseAccess", + Self::DatabaseAccessRoot => "DatabaseAccessRoot", + Self::DatabaseAccessGrant => "DatabaseAccessGrant", Self::DatabaseAnalyzer => "DatabaseAnalyzer", Self::DatabaseFunction => "DatabaseFunction", Self::DatabaseModel => "DatabaseModel", diff --git a/core/src/key/database/ac.rs b/core/src/key/database/access/ac.rs similarity index 85% rename from core/src/key/database/ac.rs rename to core/src/key/database/access/ac.rs index 76ec9e35..7b991ad1 100644 --- a/core/src/key/database/ac.rs +++ b/core/src/key/database/access/ac.rs @@ -1,4 +1,4 @@ -//! Stores a DEFINE ACCESS ON DATABASE config definition +//! Stores a DEFINE ACCESS ON DATABASE configuration use crate::key::category::Categorise; use crate::key::category::Category; use derive::Key; @@ -23,13 +23,13 @@ pub fn new<'a>(ns: &'a str, db: &'a str, ac: &'a str) -> Ac<'a> { } pub fn prefix(ns: &str, db: &str) -> Vec { - let mut k = super::all::new(ns, db).encode().unwrap(); + let mut k = crate::key::database::all::new(ns, db).encode().unwrap(); k.extend_from_slice(&[b'!', b'a', b'c', 0x00]); k } pub fn suffix(ns: &str, db: &str) -> Vec { - let mut k = super::all::new(ns, db).encode().unwrap(); + let mut k = crate::key::database::all::new(ns, db).encode().unwrap(); k.extend_from_slice(&[b'!', b'a', b'c', 0xff]); k } @@ -68,7 +68,7 @@ mod tests { "testac", ); let enc = Ac::encode(&val).unwrap(); - assert_eq!(enc, b"/*testns\x00*testdb\x00!actestac\x00"); + assert_eq!(enc, b"/*testns\0*testdb\0!actestac\0"); let dec = Ac::decode(&enc).unwrap(); assert_eq!(val, dec); diff --git a/core/src/key/database/access/all.rs b/core/src/key/database/access/all.rs new file mode 100644 index 00000000..5b148efc --- /dev/null +++ b/core/src/key/database/access/all.rs @@ -0,0 +1,60 @@ +//! Stores the key prefix for all keys under a database access method +use crate::key::category::Categorise; +use crate::key::category::Category; +use derive::Key; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)] +#[non_exhaustive] +pub struct Access<'a> { + __: u8, + _a: u8, + pub ns: &'a str, + _b: u8, + pub db: &'a str, + _c: u8, + pub ac: &'a str, +} + +pub fn new<'a>(ns: &'a str, db: &'a str, ac: &'a str) -> Access<'a> { + Access::new(ns, db, ac) +} + +impl Categorise for Access<'_> { + fn categorise(&self) -> Category { + Category::DatabaseAccessGrant + } +} + +impl<'a> Access<'a> { + pub fn new(ns: &'a str, db: &'a str, ac: &'a str) -> Self { + Self { + __: b'/', + _a: b'*', + ns, + _b: b'*', + db, + _c: b'*', + ac, + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn key() { + use super::*; + #[rustfmt::skip] + let val = Access::new( + "testns", + "testdb", + "testac", + ); + let enc = Access::encode(&val).unwrap(); + assert_eq!(enc, b"/*testns\0*testdb\0*testac\0"); + + let dec = Access::decode(&enc).unwrap(); + assert_eq!(val, dec); + } +} diff --git a/core/src/key/database/access/gr.rs b/core/src/key/database/access/gr.rs new file mode 100644 index 00000000..2b02d123 --- /dev/null +++ b/core/src/key/database/access/gr.rs @@ -0,0 +1,93 @@ +//! Stores a grant associated with an access method +use crate::key::category::Categorise; +use crate::key::category::Category; +use derive::Key; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)] +#[non_exhaustive] +pub struct Gr<'a> { + __: u8, + _a: u8, + pub ns: &'a str, + _b: u8, + pub db: &'a str, + _c: u8, + pub ac: &'a str, + _d: u8, + _e: u8, + _f: u8, + pub gr: &'a str, +} + +pub fn new<'a>(ns: &'a str, db: &'a str, ac: &'a str, gr: &'a str) -> Gr<'a> { + Gr::new(ns, db, ac, gr) +} + +pub fn prefix(ns: &str, db: &str, ac: &str) -> Vec { + let mut k = super::all::new(ns, db, ac).encode().unwrap(); + k.extend_from_slice(&[b'!', b'g', b'r', 0x00]); + k +} + +pub fn suffix(ns: &str, db: &str, ac: &str) -> Vec { + let mut k = super::all::new(ns, db, ac).encode().unwrap(); + k.extend_from_slice(&[b'!', b'g', b'r', 0xff]); + k +} + +impl Categorise for Gr<'_> { + fn categorise(&self) -> Category { + Category::DatabaseAccessGrant + } +} + +impl<'a> Gr<'a> { + pub fn new(ns: &'a str, db: &'a str, ac: &'a str, gr: &'a str) -> Self { + Self { + __: b'/', + _a: b'*', + ns, + _b: b'*', + db, + _c: b'*', + ac, + _d: b'!', + _e: b'g', + _f: b'r', + gr, + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn key() { + use super::*; + #[rustfmt::skip] + let val = Gr::new( + "testns", + "testdb", + "testac", + "testgr", + ); + let enc = Gr::encode(&val).unwrap(); + assert_eq!(enc, b"/*testns\0*testdb\0*testac\0!grtestgr\0"); + + let dec = Gr::decode(&enc).unwrap(); + assert_eq!(val, dec); + } + + #[test] + fn test_prefix() { + let val = super::prefix("testns", "testdb", "testac"); + assert_eq!(val, b"/*testns\0*testdb\0*testac\0!gr\0"); + } + + #[test] + fn test_suffix() { + let val = super::suffix("testns", "testdb", "testac"); + assert_eq!(val, b"/*testns\0*testdb\0*testac\0!gr\xff"); + } +} diff --git a/core/src/key/database/access/mod.rs b/core/src/key/database/access/mod.rs new file mode 100644 index 00000000..f88661a1 --- /dev/null +++ b/core/src/key/database/access/mod.rs @@ -0,0 +1,3 @@ +pub mod ac; +pub mod all; +pub mod gr; diff --git a/core/src/key/database/mod.rs b/core/src/key/database/mod.rs index 33923432..5384da3e 100644 --- a/core/src/key/database/mod.rs +++ b/core/src/key/database/mod.rs @@ -1,4 +1,4 @@ -pub mod ac; +pub mod access; pub mod all; pub mod az; pub mod fc; diff --git a/core/src/key/mod.rs b/core/src/key/mod.rs index b2e97002..e0322090 100644 --- a/core/src/key/mod.rs +++ b/core/src/key/mod.rs @@ -1,7 +1,9 @@ //! How the keys are structured in the key value store /// /// crate::key::root::all / -/// crate::key::root::ac /!ac{ac} +/// crate::key::root::access::all /*{ac} +/// crate::key::root::access::ac /!ac{ac} +/// crate::key::root::access::gr /*{ac}!gr{gr} /// crate::key::root::hb /!hb{ts}/{nd} /// crate::key::root::nd /!nd{nd} /// crate::key::root::ni /!ni @@ -12,14 +14,18 @@ /// crate::key::node::lq /${nd}!lq{lq}{ns}{db} /// /// crate::key::namespace::all /*{ns} -/// crate::key::namespace::ac /*{ns}!ac{ac} +/// crate::key::namespace::access::all /*{ns}*{ac} +/// crate::key::namespace::access::ac /*{ns}!ac{ac} +/// crate::key::namespace::access::gr /*{ns}*{ac}!gr{gr} /// crate::key::namespace::db /*{ns}!db{db} /// crate::key::namespace::di /+{ns id}!di /// crate::key::namespace::lg /*{ns}!lg{lg} /// crate::key::namespace::us /*{ns}!us{us} /// /// crate::key::database::all /*{ns}*{db} -/// crate::key::database::ac /*{ns}*{db}!ac{ac} +/// crate::key::database::access::all /*{ns}*{db}*{ac} +/// crate::key::database::access::ac /*{ns}*{db}!ac{ac} +/// crate::key::database::access::gr /*{ns}*{db}*{ac}!gr{gr} /// crate::key::database::az /*{ns}*{db}!az{az} /// crate::key::database::fc /*{ns}*{db}!fn{fc} /// crate::key::database::ml /*{ns}*{db}!ml{ml}{vn} diff --git a/core/src/key/namespace/ac.rs b/core/src/key/namespace/access/ac.rs similarity index 73% rename from core/src/key/namespace/ac.rs rename to core/src/key/namespace/access/ac.rs index bb5fca13..7d7d5d44 100644 --- a/core/src/key/namespace/ac.rs +++ b/core/src/key/namespace/access/ac.rs @@ -1,4 +1,4 @@ -//! Stores a DEFINE ACCESS ON NAMESPACE config definition +//! Stores a DEFINE ACCESS ON NAMESPACE configuration use crate::key::category::Categorise; use crate::key::category::Category; use derive::Key; @@ -21,13 +21,13 @@ pub fn new<'a>(ns: &'a str, ac: &'a str) -> Ac<'a> { } pub fn prefix(ns: &str) -> Vec { - let mut k = super::all::new(ns).encode().unwrap(); + let mut k = crate::key::namespace::all::new(ns).encode().unwrap(); k.extend_from_slice(&[b'!', b'a', b'c', 0x00]); k } pub fn suffix(ns: &str) -> Vec { - let mut k = super::all::new(ns).encode().unwrap(); + let mut k = crate::key::namespace::all::new(ns).encode().unwrap(); k.extend_from_slice(&[b'!', b'a', b'c', 0xff]); k } @@ -64,7 +64,20 @@ mod tests { ); let enc = Ac::encode(&val).unwrap(); assert_eq!(enc, b"/*testns\0!actestac\0"); + let dec = Ac::decode(&enc).unwrap(); assert_eq!(val, dec); } + + #[test] + fn test_prefix() { + let val = super::prefix("testns"); + assert_eq!(val, b"/*testns\0!ac\0"); + } + + #[test] + fn test_suffix() { + let val = super::suffix("testns"); + assert_eq!(val, b"/*testns\0!ac\xff"); + } } diff --git a/core/src/key/namespace/access/all.rs b/core/src/key/namespace/access/all.rs new file mode 100644 index 00000000..691faf6e --- /dev/null +++ b/core/src/key/namespace/access/all.rs @@ -0,0 +1,55 @@ +//! Stores the key prefix for all keys under a namespace access method +use crate::key::category::Categorise; +use crate::key::category::Category; +use derive::Key; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)] +#[non_exhaustive] +pub struct Access<'a> { + __: u8, + _a: u8, + pub ns: &'a str, + _b: u8, + pub ac: &'a str, +} + +pub fn new<'a>(ns: &'a str, ac: &'a str) -> Access<'a> { + Access::new(ns, ac) +} + +impl Categorise for Access<'_> { + fn categorise(&self) -> Category { + Category::NamespaceAccessRoot + } +} + +impl<'a> Access<'a> { + pub fn new(ns: &'a str, ac: &'a str) -> Self { + Self { + __: b'/', + _a: b'*', + ns, + _b: b'*', + ac, + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn key() { + use super::*; + #[rustfmt::skip] + let val = Access::new( + "testns", + "testac", + ); + let enc = Access::encode(&val).unwrap(); + assert_eq!(enc, b"/*testns\0*testac\0"); + + let dec = Access::decode(&enc).unwrap(); + assert_eq!(val, dec); + } +} diff --git a/core/src/key/namespace/access/gr.rs b/core/src/key/namespace/access/gr.rs new file mode 100644 index 00000000..3dccae0a --- /dev/null +++ b/core/src/key/namespace/access/gr.rs @@ -0,0 +1,88 @@ +//! Stores a grant associated with an access method +use crate::key::category::Categorise; +use crate::key::category::Category; +use derive::Key; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)] +#[non_exhaustive] +pub struct Gr<'a> { + __: u8, + _a: u8, + pub ns: &'a str, + _b: u8, + pub ac: &'a str, + _c: u8, + _d: u8, + _e: u8, + pub gr: &'a str, +} + +pub fn new<'a>(ns: &'a str, ac: &'a str, gr: &'a str) -> Gr<'a> { + Gr::new(ns, ac, gr) +} + +pub fn prefix(ns: &str, ac: &str) -> Vec { + let mut k = super::all::new(ns, ac).encode().unwrap(); + k.extend_from_slice(&[b'!', b'g', b'r', 0x00]); + k +} + +pub fn suffix(ns: &str, ac: &str) -> Vec { + let mut k = super::all::new(ns, ac).encode().unwrap(); + k.extend_from_slice(&[b'!', b'g', b'r', 0xff]); + k +} + +impl Categorise for Gr<'_> { + fn categorise(&self) -> Category { + Category::NamespaceAccessGrant + } +} + +impl<'a> Gr<'a> { + pub fn new(ns: &'a str, ac: &'a str, gr: &'a str) -> Self { + Self { + __: b'/', + _a: b'*', + ns, + _b: b'*', + ac, + _c: b'!', + _d: b'g', + _e: b'r', + gr, + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn key() { + use super::*; + #[rustfmt::skip] + let val = Gr::new( + "testns", + "testac", + "testgr", + ); + let enc = Gr::encode(&val).unwrap(); + assert_eq!(enc, b"/*testns\0*testac\0!grtestgr\0"); + + let dec = Gr::decode(&enc).unwrap(); + assert_eq!(val, dec); + } + + #[test] + fn test_prefix() { + let val = super::prefix("testns", "testac"); + assert_eq!(val, b"/*testns\0*testac\0!gr\0"); + } + + #[test] + fn test_suffix() { + let val = super::suffix("testns", "testac"); + assert_eq!(val, b"/*testns\0*testac\0!gr\xff"); + } +} diff --git a/core/src/key/namespace/access/mod.rs b/core/src/key/namespace/access/mod.rs new file mode 100644 index 00000000..f88661a1 --- /dev/null +++ b/core/src/key/namespace/access/mod.rs @@ -0,0 +1,3 @@ +pub mod ac; +pub mod all; +pub mod gr; diff --git a/core/src/key/namespace/mod.rs b/core/src/key/namespace/mod.rs index 2feaaeb0..132f569f 100644 --- a/core/src/key/namespace/mod.rs +++ b/core/src/key/namespace/mod.rs @@ -1,4 +1,4 @@ -pub mod ac; +pub mod access; pub mod all; pub mod db; pub mod di; diff --git a/core/src/key/root/ac.rs b/core/src/key/root/access/ac.rs similarity index 87% rename from core/src/key/root/ac.rs rename to core/src/key/root/access/ac.rs index f566eb3e..49bce3f2 100644 --- a/core/src/key/root/ac.rs +++ b/core/src/key/root/access/ac.rs @@ -1,4 +1,4 @@ -//! Stores a DEFINE ACCESS ON ROOT config definition +//! Stores a DEFINE ACCESS ON ROOT configuration use crate::key::category::Categorise; use crate::key::category::Category; use derive::Key; @@ -19,13 +19,13 @@ pub fn new(ac: &str) -> Ac<'_> { } pub fn prefix() -> Vec { - let mut k = super::all::new().encode().unwrap(); + let mut k = crate::key::root::all::new().encode().unwrap(); k.extend_from_slice(&[b'!', b'a', b'c', 0x00]); k } pub fn suffix() -> Vec { - let mut k = super::all::new().encode().unwrap(); + let mut k = crate::key::root::all::new().encode().unwrap(); k.extend_from_slice(&[b'!', b'a', b'c', 0xff]); k } diff --git a/core/src/key/root/access/all.rs b/core/src/key/root/access/all.rs new file mode 100644 index 00000000..47229bdb --- /dev/null +++ b/core/src/key/root/access/all.rs @@ -0,0 +1,50 @@ +//! Stores the key prefix for all keys under a root access method +use crate::key::category::Categorise; +use crate::key::category::Category; +use derive::Key; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)] +#[non_exhaustive] +pub struct Access<'a> { + __: u8, + _a: u8, + pub ac: &'a str, +} + +pub fn new(ac: &str) -> Access { + Access::new(ac) +} + +impl Categorise for Access<'_> { + fn categorise(&self) -> Category { + Category::AccessRoot + } +} + +impl<'a> Access<'a> { + pub fn new(ac: &'a str) -> Self { + Self { + __: b'/', + _a: b'*', + ac, + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn key() { + use super::*; + #[rustfmt::skip] + let val = Access::new( + "testac", + ); + let enc = Access::encode(&val).unwrap(); + assert_eq!(enc, b"/*testac\0"); + + let dec = Access::decode(&enc).unwrap(); + assert_eq!(val, dec); + } +} diff --git a/core/src/key/root/access/gr.rs b/core/src/key/root/access/gr.rs new file mode 100644 index 00000000..3d34fadc --- /dev/null +++ b/core/src/key/root/access/gr.rs @@ -0,0 +1,83 @@ +//! Stores a grant associated with an access method +use crate::key::category::Categorise; +use crate::key::category::Category; +use derive::Key; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)] +#[non_exhaustive] +pub struct Gr<'a> { + __: u8, + _a: u8, + pub ac: &'a str, + _b: u8, + _c: u8, + _d: u8, + pub gr: &'a str, +} + +pub fn new<'a>(ac: &'a str, gr: &'a str) -> Gr<'a> { + Gr::new(ac, gr) +} + +pub fn prefix(ac: &str) -> Vec { + let mut k = super::all::new(ac).encode().unwrap(); + k.extend_from_slice(&[b'!', b'g', b'r', 0x00]); + k +} + +pub fn suffix(ac: &str) -> Vec { + let mut k = super::all::new(ac).encode().unwrap(); + k.extend_from_slice(&[b'!', b'g', b'r', 0xff]); + k +} + +impl Categorise for Gr<'_> { + fn categorise(&self) -> Category { + Category::AccessGrant + } +} + +impl<'a> Gr<'a> { + pub fn new(ac: &'a str, gr: &'a str) -> Self { + Self { + __: b'/', + _a: b'*', + ac, + _b: b'!', + _c: b'g', + _d: b'r', + gr, + } + } +} + +#[cfg(test)] +mod tests { + #[test] + fn key() { + use super::*; + #[rustfmt::skip] + let val = Gr::new( + "testac", + "testgr", + ); + let enc = Gr::encode(&val).unwrap(); + assert_eq!(enc, b"/*testac\0!grtestgr\0"); + + let dec = Gr::decode(&enc).unwrap(); + assert_eq!(val, dec); + } + + #[test] + fn test_prefix() { + let val = super::prefix("testac"); + assert_eq!(val, b"/*testac\0!gr\0"); + } + + #[test] + fn test_suffix() { + let val = super::suffix("testac"); + assert_eq!(val, b"/*testac\0!gr\xff"); + } +} diff --git a/core/src/key/root/access/mod.rs b/core/src/key/root/access/mod.rs new file mode 100644 index 00000000..f88661a1 --- /dev/null +++ b/core/src/key/root/access/mod.rs @@ -0,0 +1,3 @@ +pub mod ac; +pub mod all; +pub mod gr; diff --git a/core/src/key/root/mod.rs b/core/src/key/root/mod.rs index 898147e5..49f34212 100644 --- a/core/src/key/root/mod.rs +++ b/core/src/key/root/mod.rs @@ -1,4 +1,4 @@ -pub mod ac; +pub mod access; pub mod all; pub mod nd; pub mod ni; diff --git a/core/src/kvs/cache.rs b/core/src/kvs/cache.rs index e12f2ee0..f4094764 100644 --- a/core/src/kvs/cache.rs +++ b/core/src/kvs/cache.rs @@ -1,5 +1,6 @@ use super::Key; use crate::dbs::node::Node; +use crate::sql::statements::AccessGrant; use crate::sql::statements::DefineAccessStatement; use crate::sql::statements::DefineAnalyzerStatement; use crate::sql::statements::DefineDatabaseStatement; @@ -53,18 +54,24 @@ pub(super) enum Entry { Rus(Arc<[DefineUserStatement]>), /// A slice of DefineAccessStatement specified at the root. Ras(Arc<[DefineAccessStatement]>), + /// A slice of AccessGrant specified at the root. + Rag(Arc<[AccessGrant]>), /// A slice of DefineNamespaceStatement specified on a namespace. Nss(Arc<[DefineNamespaceStatement]>), /// A slice of DefineUserStatement specified on a namespace. Nus(Arc<[DefineUserStatement]>), /// A slice of DefineAccessStatement specified on a namespace. Nas(Arc<[DefineAccessStatement]>), + /// A slice of AccessGrant specified at on a namespace. + Nag(Arc<[AccessGrant]>), /// A slice of DefineDatabaseStatement specified on a namespace. Dbs(Arc<[DefineDatabaseStatement]>), /// A slice of DefineAnalyzerStatement specified on a namespace. Azs(Arc<[DefineAnalyzerStatement]>), /// A slice of DefineAccessStatement specified on a database. Das(Arc<[DefineAccessStatement]>), + /// A slice of AccessGrant specified at on a database. + Dag(Arc<[AccessGrant]>), /// A slice of DefineUserStatement specified on a database. Dus(Arc<[DefineUserStatement]>), /// A slice of DefineFunctionStatement specified on a database. @@ -120,6 +127,14 @@ impl Entry { _ => unreachable!(), } } + /// Converts this cache entry into a slice of [`AccessGrant`]. + /// This panics if called on a cache entry that is not an [`Entry::Rag`]. + pub(super) fn into_rag(self) -> Arc<[AccessGrant]> { + match self { + Entry::Rag(v) => v, + _ => unreachable!(), + } + } /// Converts this cache entry into a slice of [`DefineNamespaceStatement`]. /// This panics if called on a cache entry that is not an [`Entry::Nss`]. pub(super) fn into_nss(self) -> Arc<[DefineNamespaceStatement]> { @@ -136,6 +151,14 @@ impl Entry { _ => unreachable!(), } } + /// Converts this cache entry into a slice of [`AccessGrant`]. + /// This panics if called on a cache entry that is not an [`Entry::Nag`]. + pub(super) fn into_nag(self) -> Arc<[AccessGrant]> { + match self { + Entry::Nag(v) => v, + _ => unreachable!(), + } + } /// Converts this cache entry into a slice of [`DefineUserStatement`]. /// This panics if called on a cache entry that is not an [`Entry::Nus`]. pub(super) fn into_nus(self) -> Arc<[DefineUserStatement]> { @@ -160,6 +183,14 @@ impl Entry { _ => unreachable!(), } } + /// Converts this cache entry into a slice of [`AccessGrant`]. + /// This panics if called on a cache entry that is not an [`Entry::Dag`]. + pub(super) fn into_dag(self) -> Arc<[AccessGrant]> { + match self { + Entry::Dag(v) => v, + _ => unreachable!(), + } + } /// Converts this cache entry into a slice of [`DefineUserStatement`]. /// This panics if called on a cache entry that is not an [`Entry::Dus`]. pub(super) fn into_dus(self) -> Arc<[DefineUserStatement]> { diff --git a/core/src/kvs/ds.rs b/core/src/kvs/ds.rs index 48d4403f..d6546f3e 100644 --- a/core/src/kvs/ds.rs +++ b/core/src/kvs/ds.rs @@ -810,14 +810,14 @@ impl Datastore { /// let ds = Datastore::new("memory").await?; /// let ses = Session::owner(); /// let val = Value::Future(Box::new(Future::from(Value::Bool(true)))); - /// let res = ds.evaluate(val, &ses, None).await?; + /// let res = ds.evaluate(&val, &ses, None).await?; /// Ok(()) /// } /// ``` #[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)] pub async fn evaluate( &self, - val: Value, + val: &Value, sess: &Session, vars: Variables, ) -> Result { diff --git a/core/src/kvs/tx.rs b/core/src/kvs/tx.rs index c4723922..5c65da5f 100644 --- a/core/src/kvs/tx.rs +++ b/core/src/kvs/tx.rs @@ -11,6 +11,7 @@ use crate::kvs::cache::Entry; use crate::kvs::cache::EntryWeighter; use crate::kvs::scanner::Scanner; use crate::kvs::Transactor; +use crate::sql::statements::AccessGrant; use crate::sql::statements::DefineAccessStatement; use crate::sql::statements::DefineAnalyzerStatement; use crate::sql::statements::DefineDatabaseStatement; @@ -344,13 +345,14 @@ impl Transaction { } /// Retrieve all ROOT level accesses in a datastore. + #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] pub async fn all_root_accesses(&self) -> Result, Error> { - let key = crate::key::root::ac::prefix(); + let key = crate::key::root::access::ac::prefix(); let res = self.cache.get_value_or_guard_async(&key).await; Ok(match res { Ok(val) => val, Err(cache) => { - let end = crate::key::root::ac::suffix(); + let end = crate::key::root::access::ac::suffix(); let val = self.getr(key..end).await?; let val = val.convert().into(); let val = Entry::Ras(Arc::clone(&val)); @@ -361,6 +363,25 @@ impl Transaction { .into_ras()) } + /// Retrieve all root access grants in a datastore. + #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] + pub async fn all_root_access_grants(&self, ra: &str) -> Result, Error> { + let key = crate::key::root::access::gr::prefix(ra); + let res = self.cache.get_value_or_guard_async(&key).await; + Ok(match res { + Ok(val) => val, + Err(cache) => { + let end = crate::key::root::access::gr::suffix(ra); + let val = self.getr(key..end).await?; + let val = val.convert().into(); + let val = Entry::Rag(Arc::clone(&val)); + let _ = cache.insert(val.clone()); + val + } + } + .into_rag()) + } + /// Retrieve all namespace definitions in a datastore. #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] pub async fn all_ns(&self) -> Result, Error> { @@ -402,12 +423,12 @@ impl Transaction { /// Retrieve all namespace access definitions for a specific namespace. #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] pub async fn all_ns_accesses(&self, ns: &str) -> Result, Error> { - let key = crate::key::namespace::ac::prefix(ns); + let key = crate::key::namespace::access::ac::prefix(ns); let res = self.cache.get_value_or_guard_async(&key).await; Ok(match res { Ok(val) => val, Err(cache) => { - let end = crate::key::namespace::ac::suffix(ns); + let end = crate::key::namespace::access::ac::suffix(ns); let val = self.getr(key..end).await?; let val = val.convert().into(); let val = Entry::Nas(Arc::clone(&val)); @@ -418,6 +439,29 @@ impl Transaction { .into_nas()) } + /// Retrieve all namespace access grants for a specific namespace. + #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] + pub async fn all_ns_access_grants( + &self, + ns: &str, + na: &str, + ) -> Result, Error> { + let key = crate::key::namespace::access::gr::prefix(ns, na); + let res = self.cache.get_value_or_guard_async(&key).await; + Ok(match res { + Ok(val) => val, + Err(cache) => { + let end = crate::key::namespace::access::gr::suffix(ns, na); + let val = self.getr(key..end).await?; + let val = val.convert().into(); + let val = Entry::Nag(Arc::clone(&val)); + let _ = cache.insert(val.clone()); + val + } + } + .into_nag()) + } + /// Retrieve all database definitions for a specific namespace. #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] pub async fn all_db(&self, ns: &str) -> Result, Error> { @@ -467,12 +511,12 @@ impl Transaction { ns: &str, db: &str, ) -> Result, Error> { - let key = crate::key::database::ac::prefix(ns, db); + let key = crate::key::database::access::ac::prefix(ns, db); let res = self.cache.get_value_or_guard_async(&key).await; Ok(match res { Ok(val) => val, Err(cache) => { - let end = crate::key::database::ac::suffix(ns, db); + let end = crate::key::database::access::ac::suffix(ns, db); let val = self.getr(key..end).await?; let val = val.convert().into(); let val = Entry::Das(Arc::clone(&val)); @@ -483,6 +527,30 @@ impl Transaction { .into_das()) } + /// Retrieve all database access grants for a specific database. + #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] + pub async fn all_db_access_grants( + &self, + ns: &str, + db: &str, + da: &str, + ) -> Result, Error> { + let key = crate::key::database::access::gr::prefix(ns, db, da); + let res = self.cache.get_value_or_guard_async(&key).await; + Ok(match res { + Ok(val) => val, + Err(cache) => { + let end = crate::key::database::access::gr::suffix(ns, db, da); + let val = self.getr(key..end).await?; + let val = val.convert().into(); + let val = Entry::Dag(Arc::clone(&val)); + let _ = cache.insert(val.clone()); + val + } + } + .into_dag()) + } + /// Retrieve all analyzer definitions for a specific database. #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] pub async fn all_db_analyzers( @@ -734,7 +802,7 @@ impl Transaction { .into_type()) } - /// Retrieve a specific namespace user definition. + /// Retrieve a specific root user definition. #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] pub async fn get_root_user(&self, us: &str) -> Result, Error> { let key = crate::key::root::us::new(us).encode()?; @@ -754,15 +822,16 @@ impl Transaction { .into_type()) } - /// Retrieve a specific namespace user definition. - pub async fn get_root_access(&self, user: &str) -> Result, Error> { - let key = crate::key::root::ac::new(user).encode()?; + /// Retrieve a specific root access definition. + #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] + pub async fn get_root_access(&self, ra: &str) -> Result, Error> { + let key = crate::key::root::access::ac::new(ra).encode()?; let res = self.cache.get_value_or_guard_async(&key).await; Ok(match res { Ok(val) => val, Err(cache) => { let val = self.get(key).await?.ok_or(Error::AccessRootNotFound { - value: user.to_owned(), + ac: ra.to_owned(), })?; let val: DefineAccessStatement = val.into(); let val = Entry::Any(Arc::new(val)); @@ -773,6 +842,31 @@ impl Transaction { .into_type()) } + /// Retrieve a specific root access grant. + #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] + pub async fn get_root_access_grant( + &self, + ac: &str, + gr: &str, + ) -> Result, Error> { + let key = crate::key::root::access::gr::new(ac, gr).encode()?; + let res = self.cache.get_value_or_guard_async(&key).await; + Ok(match res { + Ok(val) => val, + Err(cache) => { + let val = self.get(key).await?.ok_or(Error::AccessGrantRootNotFound { + ac: ac.to_owned(), + gr: gr.to_owned(), + })?; + let val: AccessGrant = val.into(); + let val = Entry::Any(Arc::new(val)); + let _ = cache.insert(val.clone()); + val + } + } + .into_type()) + } + /// Retrieve a specific namespace definition. #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] pub async fn get_ns(&self, ns: &str) -> Result, Error> { @@ -821,13 +915,13 @@ impl Transaction { ns: &str, na: &str, ) -> Result, Error> { - let key = crate::key::namespace::ac::new(ns, na).encode()?; + let key = crate::key::namespace::access::ac::new(ns, na).encode()?; let res = self.cache.get_value_or_guard_async(&key).await; Ok(match res { Ok(val) => val, Err(cache) => { let val = self.get(key).await?.ok_or(Error::AccessNsNotFound { - value: na.to_owned(), + ac: na.to_owned(), ns: ns.to_owned(), })?; let val: DefineAccessStatement = val.into(); @@ -839,6 +933,33 @@ impl Transaction { .into_type()) } + /// Retrieve a specific namespace access grant. + #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] + pub async fn get_ns_access_grant( + &self, + ns: &str, + ac: &str, + gr: &str, + ) -> Result, Error> { + let key = crate::key::namespace::access::gr::new(ns, ac, gr).encode()?; + let res = self.cache.get_value_or_guard_async(&key).await; + Ok(match res { + Ok(val) => val, + Err(cache) => { + let val = self.get(key).await?.ok_or(Error::AccessGrantNsNotFound { + ac: ac.to_owned(), + gr: gr.to_owned(), + ns: ns.to_owned(), + })?; + let val: AccessGrant = val.into(); + let val = Entry::Any(Arc::new(val)); + let _ = cache.insert(val.clone()); + val + } + } + .into_type()) + } + /// Retrieve a specific database definition. #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] pub async fn get_db(&self, ns: &str, db: &str) -> Result, Error> { @@ -894,13 +1015,13 @@ impl Transaction { db: &str, da: &str, ) -> Result, Error> { - let key = crate::key::database::ac::new(ns, db, da).encode()?; + let key = crate::key::database::access::ac::new(ns, db, da).encode()?; let res = self.cache.get_value_or_guard_async(&key).await; Ok(match res { Ok(val) => val, Err(cache) => { let val = self.get(key).await?.ok_or(Error::AccessDbNotFound { - value: da.to_owned(), + ac: da.to_owned(), ns: ns.to_owned(), db: db.to_owned(), })?; @@ -913,6 +1034,35 @@ impl Transaction { .into_type()) } + /// Retrieve a specific database access grant. + #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] + pub async fn get_db_access_grant( + &self, + ns: &str, + db: &str, + ac: &str, + gr: &str, + ) -> Result, Error> { + let key = crate::key::database::access::gr::new(ns, db, ac, gr).encode()?; + let res = self.cache.get_value_or_guard_async(&key).await; + Ok(match res { + Ok(val) => val, + Err(cache) => { + let val = self.get(key).await?.ok_or(Error::AccessGrantDbNotFound { + ac: ac.to_owned(), + gr: gr.to_owned(), + ns: ns.to_owned(), + db: db.to_owned(), + })?; + let val: AccessGrant = val.into(); + let val = Entry::Any(Arc::new(val)); + let _ = cache.insert(val.clone()); + val + } + } + .into_type()) + } + /// Retrieve a specific model definition from a database. #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] pub async fn get_db_model( diff --git a/core/src/sql/access_type.rs b/core/src/sql/access_type.rs index a458deda..a935a4f4 100644 --- a/core/src/sql/access_type.rs +++ b/core/src/sql/access_type.rs @@ -9,13 +9,22 @@ use std::fmt; use std::fmt::Display; /// The type of access methods available -#[revisioned(revision = 1)] +#[revisioned(revision = 2)] #[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] pub enum AccessType { Record(RecordAccess), Jwt(JwtAccess), + // TODO(gguillemas): Document once bearer access is no longer experimental. + #[doc(hidden)] + #[revision(start = 2)] + Bearer(BearerAccess), +} + +// Allows retrieving the JWT configuration for any access type. +pub trait Jwt { + fn jwt(&self) -> &JwtAccess; } impl Default for AccessType { @@ -27,6 +36,16 @@ impl Default for AccessType { } } +impl Jwt for AccessType { + fn jwt(&self) -> &JwtAccess { + match self { + AccessType::Record(at) => at.jwt(), + AccessType::Jwt(at) => at.jwt(), + AccessType::Bearer(at) => at.jwt(), + } + } +} + impl Display for AccessType { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { @@ -43,6 +62,12 @@ impl Display for AccessType { } write!(f, " WITH JWT {}", ac.jwt)?; } + AccessType::Bearer(ac) => { + write!(f, "BEARER")?; + if let BearerAccessLevel::Record = ac.level { + write!(f, " FOR RECORD")?; + } + } } Ok(()) } @@ -61,11 +86,21 @@ impl InfoStructure for AccessType { "signup".to_string(), if let Some(v) = v.signup => v.structure(), "signin".to_string(), if let Some(v) = v.signin => v.structure(), }), + AccessType::Bearer(ac) => Value::from(map! { + "kind".to_string() => "BEARER".into(), + "level".to_string() => match ac.level { + BearerAccessLevel::Record => "RECORD", + BearerAccessLevel::User => "USER", + }.into(), + "jwt".to_string() => ac.jwt.structure(), + }), } } } impl AccessType { + // TODO(gguillemas): Document once bearer access is no longer experimental. + #[doc(hidden)] /// Returns whether or not the access method can issue non-token grants /// In this context, token refers exclusively to JWT #[allow(unreachable_patterns)] @@ -73,8 +108,7 @@ impl AccessType { match self { // The grants for JWT and record access methods are JWT AccessType::Jwt(_) | AccessType::Record(_) => false, - // TODO(gguillemas): This arm should be reachable by the bearer access method - _ => unreachable!(), + AccessType::Bearer(_) => true, } } /// Returns whether or not the access method can issue tokens @@ -118,6 +152,12 @@ impl Default for JwtAccess { } } +impl Jwt for JwtAccess { + fn jwt(&self) -> &JwtAccess { + self + } +} + impl Display for JwtAccess { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match &self.verify { @@ -296,3 +336,45 @@ impl Default for RecordAccess { } } } + +impl Jwt for RecordAccess { + fn jwt(&self) -> &JwtAccess { + &self.jwt + } +} + +// TODO(gguillemas): Document once bearer access is no longer experimental. +#[doc(hidden)] +#[revisioned(revision = 1)] +#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +pub struct BearerAccess { + pub level: BearerAccessLevel, + pub jwt: JwtAccess, +} + +impl Default for BearerAccess { + fn default() -> Self { + Self { + level: BearerAccessLevel::User, + jwt: JwtAccess { + ..Default::default() + }, + } + } +} + +impl Jwt for BearerAccess { + fn jwt(&self) -> &JwtAccess { + &self.jwt + } +} + +#[revisioned(revision = 1)] +#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub enum BearerAccessLevel { + Record, + User, +} diff --git a/core/src/sql/statement.rs b/core/src/sql/statement.rs index d2782a23..36543c79 100644 --- a/core/src/sql/statement.rs +++ b/core/src/sql/statement.rs @@ -3,6 +3,7 @@ use crate::dbs::Options; use crate::doc::CursorDoc; use crate::err::Error; use crate::sql::statements::rebuild::RebuildStatement; +use crate::sql::statements::AccessStatement; use crate::sql::{ fmt::{Fmt, Pretty}, statements::{ @@ -55,7 +56,7 @@ impl Display for Statements { } } -#[revisioned(revision = 4)] +#[revisioned(revision = 5)] #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] @@ -93,6 +94,10 @@ pub enum Statement { Upsert(UpsertStatement), #[revision(start = 4)] Alter(AlterStatement), + // TODO(gguillemas): Document once bearer access is no longer experimental. + #[doc(hidden)] + #[revision(start = 5)] + Access(AccessStatement), } impl Statement { @@ -113,6 +118,7 @@ impl Statement { pub(crate) fn writeable(&self) -> bool { match self { Self::Value(v) => v.writeable(), + Self::Access(_) => true, Self::Alter(_) => true, Self::Analyze(_) => false, Self::Break(_) => false, @@ -151,6 +157,7 @@ impl Statement { doc: Option<&CursorDoc<'_>>, ) -> Result { match self { + Self::Access(v) => v.compute(ctx, opt, doc).await, Self::Alter(v) => v.compute(stk, ctx, opt, doc).await, Self::Analyze(v) => v.compute(ctx, opt, doc).await, Self::Break(v) => v.compute(ctx, opt, doc).await, @@ -190,6 +197,7 @@ impl Display for Statement { fn fmt(&self, f: &mut Formatter) -> fmt::Result { match self { Self::Value(v) => write!(Pretty::from(f), "{v}"), + Self::Access(v) => write!(Pretty::from(f), "{v}"), Self::Alter(v) => write!(Pretty::from(f), "{v}"), Self::Analyze(v) => write!(Pretty::from(f), "{v}"), Self::Begin(v) => write!(Pretty::from(f), "{v}"), diff --git a/core/src/sql/statements/access.rs b/core/src/sql/statements/access.rs new file mode 100644 index 00000000..a84d7c8f --- /dev/null +++ b/core/src/sql/statements/access.rs @@ -0,0 +1,628 @@ +use crate::ctx::Context; +use crate::dbs::Options; +use crate::doc::CursorDoc; +use crate::err::Error; +use crate::iam::{Action, ResourceKind}; +use crate::sql::access_type::BearerAccessLevel; +use crate::sql::{AccessType, Array, Base, Datetime, Id, Ident, Object, Strand, Uuid, Value}; +use derive::Store; +use rand::Rng; +use revision::revisioned; +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::fmt::{Display, Formatter}; + +pub static GRANT_BEARER_PREFIX: &str = "surreal-bearer"; +// Keys and their identifiers are generated randomly from a 62-character pool. +pub static GRANT_BEARER_CHARACTER_POOL: &[u8] = + b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; +// The key identifier should not have collisions to prevent confusion. +// However, collisions should be handled gracefully when issuing grants. +// With 12 characters from the pool, the key identifier part has ~70 bits of entropy. +pub static GRANT_BEARER_ID_LENGTH: usize = 12; +// With 24 characters from the pool, the key part has ~140 bits of entropy. +pub static GRANT_BEARER_KEY_LENGTH: usize = 24; +// Total bearer key length. +pub static GRANT_BEARER_LENGTH: usize = + GRANT_BEARER_PREFIX.len() + 1 + GRANT_BEARER_ID_LENGTH + 1 + GRANT_BEARER_KEY_LENGTH; + +// TODO(gguillemas): Document once bearer access is no longer experimental. +#[doc(hidden)] +#[revisioned(revision = 1)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub enum AccessStatement { + Grant(AccessStatementGrant), // Create access grant. + List(AccessStatementList), // List access grants. + Revoke(AccessStatementRevoke), // Revoke access grant. + Prune(Ident), // Prune access grants. +} + +// TODO(gguillemas): Document once bearer access is no longer experimental. +#[doc(hidden)] +#[revisioned(revision = 1)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub struct AccessStatementList { + pub ac: Ident, + pub base: Option, +} + +// TODO(gguillemas): Document once bearer access is no longer experimental. +#[doc(hidden)] +#[revisioned(revision = 1)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub struct AccessStatementGrant { + pub ac: Ident, + pub base: Option, + pub subject: Option, +} + +// TODO(gguillemas): Document once bearer access is no longer experimental. +#[doc(hidden)] +#[revisioned(revision = 1)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub struct AccessStatementRevoke { + pub ac: Ident, + pub base: Option, + pub gr: Ident, +} + +// TODO(gguillemas): Document once bearer access is no longer experimental. +#[doc(hidden)] +#[revisioned(revision = 1)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub struct AccessGrant { + pub id: Ident, // Unique grant identifier. + pub ac: Ident, // Access method used to create the grant. + pub creation: Datetime, // Grant creation time. + pub expiration: Option, // Grant expiration time, if any. + pub revocation: Option, // Grant revocation time, if any. + pub subject: Option, // Subject of the grant. + pub grant: Grant, // Grant data. +} + +impl AccessGrant { + /// Returns a version of the statement where potential secrets are redacted. + /// This function should be used when displaying the statement to datastore users. + /// This function should NOT be used when displaying the statement for export purposes. + pub fn redacted(&self) -> AccessGrant { + let mut ags = self.clone(); + ags.grant = match ags.grant { + Grant::Jwt(mut gr) => { + // Token should not even be stored. We clear it just as a precaution. + gr.token = None; + Grant::Jwt(gr) + } + Grant::Record(mut gr) => { + // Token should not even be stored. We clear it just as a precaution. + gr.token = None; + Grant::Record(gr) + } + Grant::Bearer(mut gr) => { + // Key is stored, but should not usually be displayed. + gr.key = "[REDACTED]".into(); + Grant::Bearer(gr) + } + }; + ags + } +} + +impl From for Object { + fn from(grant: AccessGrant) -> Self { + let mut res = Object::default(); + res.insert("id".to_owned(), Value::from(grant.id.to_raw())); + res.insert("ac".to_owned(), Value::from(grant.ac.to_string())); + res.insert("creation".to_owned(), Value::from(grant.creation)); + res.insert("expiration".to_owned(), Value::from(grant.expiration)); + res.insert("revocation".to_owned(), Value::from(grant.revocation)); + if let Some(subject) = grant.subject { + let mut sub = Object::default(); + match subject { + Subject::Record(id) => sub.insert("record".to_owned(), Value::from(id)), + Subject::User(name) => sub.insert("user".to_owned(), Value::from(name.to_string())), + }; + res.insert("subject".to_owned(), Value::from(sub)); + } + + let mut gr = Object::default(); + match grant.grant { + Grant::Jwt(jg) => { + gr.insert("jti".to_owned(), Value::from(jg.jti)); + if let Some(token) = jg.token { + gr.insert("token".to_owned(), Value::from(token)); + } + } + Grant::Record(rg) => { + gr.insert("rid".to_owned(), Value::from(rg.rid)); + gr.insert("jti".to_owned(), Value::from(rg.jti)); + if let Some(token) = rg.token { + gr.insert("token".to_owned(), Value::from(token)); + } + } + Grant::Bearer(bg) => { + gr.insert("id".to_owned(), Value::from(bg.id.to_raw())); + gr.insert("key".to_owned(), Value::from(bg.key)); + } + }; + res.insert("grant".to_owned(), Value::from(gr)); + + res + } +} + +#[revisioned(revision = 1)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub enum Subject { + Record(Id), + User(Ident), +} + +#[revisioned(revision = 1)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub enum Grant { + Jwt(GrantJwt), + Record(GrantRecord), + Bearer(GrantBearer), +} + +#[revisioned(revision = 1)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub struct GrantJwt { + pub jti: Uuid, // JWT ID + pub token: Option, // JWT. Will not be stored after being returned. +} + +#[revisioned(revision = 1)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub struct GrantRecord { + pub rid: Uuid, // Record ID + pub jti: Uuid, // JWT ID + pub token: Option, // JWT. Will not be stored after being returned. +} + +#[revisioned(revision = 1)] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[non_exhaustive] +pub struct GrantBearer { + pub id: Ident, // Key ID + pub key: Strand, // Key. Will be stored but afterwards returned redacted. +} + +impl GrantBearer { + #[doc(hidden)] + pub fn new() -> Self { + let id = random_string(GRANT_BEARER_ID_LENGTH); + let secret = random_string(GRANT_BEARER_KEY_LENGTH); + Self { + id: id.clone().into(), + key: format!("{GRANT_BEARER_PREFIX}-{id}-{secret}").into(), + } + } +} + +fn random_string(length: usize) -> String { + let mut rng = rand::thread_rng(); + let string: String = (0..length) + .map(|_| { + let i = rng.gen_range(0..GRANT_BEARER_CHARACTER_POOL.len()); + GRANT_BEARER_CHARACTER_POOL[i] as char + }) + .collect(); + string +} + +async fn compute_grant( + stmt: &AccessStatementGrant, + ctx: &Context<'_>, + opt: &Options, + _doc: Option<&CursorDoc<'_>>, +) -> Result { + let base = match &stmt.base { + Some(base) => base.clone(), + None => opt.selected_base()?, + }; + // Allowed to run? + opt.is_allowed(Action::Edit, ResourceKind::Access, &base)?; + match base { + Base::Root => { + // Get the transaction + let txn = ctx.tx(); + // Clear the cache + txn.clear(); + // Read the access definition + let ac = txn.get_root_access(&stmt.ac.to_raw()).await?; + // Verify the access type + match &ac.kind { + AccessType::Jwt(_) => Err(Error::FeatureNotYetImplemented { + feature: "Grants for JWT on namespace".to_string(), + }), + AccessType::Bearer(at) => { + match &stmt.subject { + Some(Subject::User(user)) => { + // Grant subject must match access method level. + if !matches!(&at.level, BearerAccessLevel::User) { + return Err(Error::AccessGrantInvalidSubject); + } + // If the grant is being created for a user, the user must exist. + txn.get_root_user(user).await?; + } + Some(Subject::Record(_)) => { + // If the grant is being created for a record, a database must be selected. + return Err(Error::DbEmpty); + } + None => return Err(Error::AccessGrantInvalidSubject), + } + // Create a new bearer key. + let grant = GrantBearer::new(); + let gr = AccessGrant { + ac: ac.name.clone(), + // Unique grant identifier. + // In the case of bearer grants, the key identifier. + id: grant.id.clone(), + // Current time. + creation: Datetime::default(), + // Current time plus grant duration. Only if set. + expiration: ac.duration.grant.map(|d| d + Datetime::default()), + // The grant is initially not revoked. + revocation: None, + // Subject associated with the grant. + subject: stmt.subject.to_owned(), + // The contents of the grant. + grant: Grant::Bearer(grant), + }; + let ac_str = gr.ac.to_raw(); + let gr_str = gr.id.to_raw(); + // Process the statement + let key = crate::key::root::access::gr::new(&ac_str, &gr_str); + txn.set(key, &gr).await?; + Ok(Value::Object(gr.into())) + } + _ => Err(Error::AccessMethodMismatch), + } + } + Base::Ns => { + // Get the transaction + let txn = ctx.tx(); + // Clear the cache + txn.clear(); + // Read the access definition + let ac = txn.get_ns_access(opt.ns()?, &stmt.ac.to_raw()).await?; + // Verify the access type + match &ac.kind { + AccessType::Jwt(_) => Err(Error::FeatureNotYetImplemented { + feature: "Grants for JWT on namespace".to_string(), + }), + AccessType::Bearer(at) => { + match &stmt.subject { + Some(Subject::User(user)) => { + // Grant subject must match access method level. + if !matches!(&at.level, BearerAccessLevel::User) { + return Err(Error::AccessGrantInvalidSubject); + } + // If the grant is being created for a user, the user must exist. + txn.get_ns_user(opt.ns()?, user).await?; + } + Some(Subject::Record(_)) => { + // If the grant is being created for a record, a database must be selected. + return Err(Error::DbEmpty); + } + None => return Err(Error::AccessGrantInvalidSubject), + } + // Create a new bearer key. + let grant = GrantBearer::new(); + let gr = AccessGrant { + ac: ac.name.clone(), + // Unique grant identifier. + // In the case of bearer grants, the key identifier. + id: grant.id.clone(), + // Current time. + creation: Datetime::default(), + // Current time plus grant duration. Only if set. + expiration: ac.duration.grant.map(|d| d + Datetime::default()), + // The grant is initially not revoked. + revocation: None, + // Subject associated with the grant. + subject: stmt.subject.to_owned(), + // The contents of the grant. + grant: Grant::Bearer(grant), + }; + let ac_str = gr.ac.to_raw(); + let gr_str = gr.id.to_raw(); + // Process the statement + let key = crate::key::namespace::access::gr::new(opt.ns()?, &ac_str, &gr_str); + txn.get_or_add_ns(opt.ns()?, opt.strict).await?; + txn.set(key, &gr).await?; + Ok(Value::Object(gr.into())) + } + _ => Err(Error::AccessMethodMismatch), + } + } + Base::Db => { + // Get the transaction + let txn = ctx.tx(); + // Clear the cache + txn.clear(); + // Read the access definition + let ac = txn.get_db_access(opt.ns()?, opt.db()?, &stmt.ac.to_raw()).await?; + // Verify the access type + match &ac.kind { + AccessType::Jwt(_) => Err(Error::FeatureNotYetImplemented { + feature: "Grants for JWT on database".to_string(), + }), + AccessType::Record(_) => Err(Error::FeatureNotYetImplemented { + feature: "Grants for record on database".to_string(), + }), + AccessType::Bearer(at) => { + match &stmt.subject { + Some(Subject::User(user)) => { + // Grant subject must match access method level. + if !matches!(&at.level, BearerAccessLevel::User) { + return Err(Error::AccessGrantInvalidSubject); + } + // If the grant is being created for a user, the user must exist. + txn.get_db_user(opt.ns()?, opt.db()?, user).await?; + } + Some(Subject::Record(_)) => { + // Grant subject must match access method level. + if !matches!(&at.level, BearerAccessLevel::Record) { + return Err(Error::AccessGrantInvalidSubject); + } + } + None => return Err(Error::AccessGrantInvalidSubject), + } + // Create a new bearer key. + let grant = GrantBearer::new(); + let gr = AccessGrant { + ac: ac.name.clone(), + // Unique grant identifier. + // In the case of bearer grants, the key identifier. + id: grant.id.clone(), + // Current time. + creation: Datetime::default(), + // Current time plus grant duration. Only if set. + expiration: ac.duration.grant.map(|d| d + Datetime::default()), + // The grant is initially not revoked. + revocation: None, + // Subject associated with the grant. + subject: stmt.subject.clone(), + // The contents of the grant. + grant: Grant::Bearer(grant), + }; + let ac_str = gr.ac.to_raw(); + let gr_str = gr.id.to_raw(); + // Process the statement + let key = crate::key::database::access::gr::new( + opt.ns()?, + opt.db()?, + &ac_str, + &gr_str, + ); + txn.get_or_add_ns(opt.ns()?, opt.strict).await?; + txn.get_or_add_db(opt.ns()?, opt.db()?, opt.strict).await?; + txn.set(key, &gr).await?; + Ok(Value::Object(gr.into())) + } + } + } + _ => Err(Error::Unimplemented( + "Managing access methods outside of root, namespace and database levels".to_string(), + )), + } +} + +async fn compute_list( + stmt: &AccessStatementList, + ctx: &Context<'_>, + opt: &Options, + _doc: Option<&CursorDoc<'_>>, +) -> Result { + let base = match &stmt.base { + Some(base) => base.clone(), + None => opt.selected_base()?, + }; + // Allowed to run? + opt.is_allowed(Action::View, ResourceKind::Access, &base)?; + match base { + Base::Root => { + // Get the transaction + let txn = ctx.tx(); + // Clear the cache + txn.clear(); + // Check if the access method exists. + txn.get_root_access(&stmt.ac).await?; + // Get the grants for the access method. + let mut grants = Array::default(); + // Show redacted version of the access grants. + for v in txn.all_root_access_grants(&stmt.ac).await?.iter() { + grants = grants + Value::Object(v.redacted().to_owned().into()); + } + Ok(Value::Array(grants)) + } + Base::Ns => { + // Get the transaction + let txn = ctx.tx(); + // Clear the cache + txn.clear(); + // Check if the access method exists. + txn.get_ns_access(opt.ns()?, &stmt.ac).await?; + // Get the grants for the access method. + let mut grants = Array::default(); + // Show redacted version of the access grants. + for v in txn.all_ns_access_grants(opt.ns()?, &stmt.ac).await?.iter() { + grants = grants + Value::Object(v.redacted().to_owned().into()); + } + Ok(Value::Array(grants)) + } + Base::Db => { + // Get the transaction + let txn = ctx.tx(); + // Clear the cache + txn.clear(); + // Check if the access method exists. + txn.get_db_access(opt.ns()?, opt.db()?, &stmt.ac).await?; + // Get the grants for the access method. + let mut grants = Array::default(); + // Show redacted version of the access grants. + for v in txn.all_db_access_grants(opt.ns()?, opt.db()?, &stmt.ac).await?.iter() { + grants = grants + Value::Object(v.redacted().to_owned().into()); + } + Ok(Value::Array(grants)) + } + _ => Err(Error::Unimplemented( + "Managing access methods outside of root, namespace and database levels".to_string(), + )), + } +} + +async fn compute_revoke( + stmt: &AccessStatementRevoke, + ctx: &Context<'_>, + opt: &Options, + _doc: Option<&CursorDoc<'_>>, +) -> Result { + let base = match &stmt.base { + Some(base) => base.clone(), + None => opt.selected_base()?, + }; + // Allowed to run? + opt.is_allowed(Action::Edit, ResourceKind::Access, &base)?; + match base { + Base::Root => { + // Get the transaction + let txn = ctx.tx(); + // Clear the cache + txn.clear(); + // Check if the access method exists. + txn.get_root_access(&stmt.ac).await?; + // Get the grants to revoke + let ac_str = stmt.ac.to_raw(); + let gr_str = stmt.gr.to_raw(); + let mut gr = (*txn.get_root_access_grant(&ac_str, &gr_str).await?).clone(); + if gr.revocation.is_some() { + return Err(Error::AccessGrantRevoked); + } + gr.revocation = Some(Datetime::default()); + // Process the statement + let key = crate::key::root::access::gr::new(&ac_str, &gr_str); + txn.set(key, &gr).await?; + Ok(Value::Object(gr.redacted().into())) + } + Base::Ns => { + // Get the transaction + let txn = ctx.tx(); + // Clear the cache + txn.clear(); + // Check if the access method exists. + txn.get_ns_access(opt.ns()?, &stmt.ac).await?; + // Get the grants to revoke + let ac_str = stmt.ac.to_raw(); + let gr_str = stmt.gr.to_raw(); + let mut gr = (*txn.get_ns_access_grant(opt.ns()?, &ac_str, &gr_str).await?).clone(); + if gr.revocation.is_some() { + return Err(Error::AccessGrantRevoked); + } + gr.revocation = Some(Datetime::default()); + // Process the statement + let key = crate::key::namespace::access::gr::new(opt.ns()?, &ac_str, &gr_str); + txn.get_or_add_ns(opt.ns()?, opt.strict).await?; + txn.set(key, &gr).await?; + Ok(Value::Object(gr.redacted().into())) + } + Base::Db => { + // Get the transaction + let txn = ctx.tx(); + // Clear the cache + txn.clear(); + // Check if the access method exists. + txn.get_db_access(opt.ns()?, opt.db()?, &stmt.ac).await?; + // Get the grants to revoke + let ac_str = stmt.ac.to_raw(); + let gr_str = stmt.gr.to_raw(); + let mut gr = + (*txn.get_db_access_grant(opt.ns()?, opt.db()?, &ac_str, &gr_str).await?).clone(); + if gr.revocation.is_some() { + return Err(Error::AccessGrantRevoked); + } + gr.revocation = Some(Datetime::default()); + // Process the statement + let key = crate::key::database::access::gr::new(opt.ns()?, opt.db()?, &ac_str, &gr_str); + txn.get_or_add_ns(opt.ns()?, opt.strict).await?; + txn.get_or_add_db(opt.ns()?, opt.db()?, opt.strict).await?; + txn.set(key, &gr).await?; + Ok(Value::Object(gr.redacted().into())) + } + _ => Err(Error::Unimplemented( + "Managing access methods outside of root, namespace and database levels".to_string(), + )), + } +} + +impl AccessStatement { + /// Process this type returning a computed simple Value + pub(crate) async fn compute( + &self, + ctx: &Context<'_>, + opt: &Options, + _doc: Option<&CursorDoc<'_>>, + ) -> Result { + match self { + AccessStatement::Grant(stmt) => compute_grant(stmt, ctx, opt, _doc).await, + AccessStatement::List(stmt) => compute_list(stmt, ctx, opt, _doc).await, + AccessStatement::Revoke(stmt) => compute_revoke(stmt, ctx, opt, _doc).await, + AccessStatement::Prune(_) => Err(Error::FeatureNotYetImplemented { + feature: "Pruning disabled grants".to_string(), + }), + } + } +} + +impl Display for AccessStatement { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + Self::Grant(stmt) => { + write!(f, "ACCESS {}", stmt.ac)?; + if let Some(ref v) = stmt.base { + write!(f, " ON {v}")?; + } + write!(f, "GRANT")?; + Ok(()) + } + Self::List(stmt) => { + write!(f, "ACCESS {}", stmt.ac)?; + if let Some(ref v) = stmt.base { + write!(f, " ON {v}")?; + } + write!(f, "LIST")?; + Ok(()) + } + Self::Revoke(stmt) => { + write!(f, "ACCESS {}", stmt.ac)?; + if let Some(ref v) = stmt.base { + write!(f, " ON {v}")?; + } + write!(f, "REVOKE {}", stmt.gr)?; + Ok(()) + } + Self::Prune(stmt) => write!(f, "ACCESS {} PRUNE", stmt), + } + } +} diff --git a/core/src/sql/statements/define/access.rs b/core/src/sql/statements/define/access.rs index 801280e6..3938adc0 100644 --- a/core/src/sql/statements/define/access.rs +++ b/core/src/sql/statements/define/access.rs @@ -48,6 +48,10 @@ impl DefineAccessStatement { ac.jwt = ac.jwt.redacted(); AccessType::Record(ac) } + AccessType::Bearer(mut ac) => { + ac.jwt = ac.jwt.redacted(); + AccessType::Bearer(ac) + } }; das } @@ -74,12 +78,12 @@ impl DefineAccessStatement { return Ok(Value::None); } else if !self.overwrite { return Err(Error::AccessRootAlreadyExists { - value: self.name.to_string(), + ac: self.name.to_string(), }); } } // Process the statement - let key = crate::key::root::ac::new(&self.name); + let key = crate::key::root::access::ac::new(&self.name); txn.set( key, DefineAccessStatement { @@ -104,13 +108,13 @@ impl DefineAccessStatement { return Ok(Value::None); } else if !self.overwrite { return Err(Error::AccessNsAlreadyExists { - value: self.name.to_string(), + ac: self.name.to_string(), ns: opt.ns()?.into(), }); } } // Process the statement - let key = crate::key::namespace::ac::new(opt.ns()?, &self.name); + let key = crate::key::namespace::access::ac::new(opt.ns()?, &self.name); txn.get_or_add_ns(opt.ns()?, opt.strict).await?; txn.set( key, @@ -136,14 +140,14 @@ impl DefineAccessStatement { return Ok(Value::None); } else if !self.overwrite { return Err(Error::AccessDbAlreadyExists { - value: self.name.to_string(), + ac: self.name.to_string(), ns: opt.ns()?.into(), db: opt.db()?.into(), }); } } // Process the statement - let key = crate::key::database::ac::new(opt.ns()?, opt.db()?, &self.name); + let key = crate::key::database::access::ac::new(opt.ns()?, opt.db()?, &self.name); txn.get_or_add_ns(opt.ns()?, opt.strict).await?; txn.get_or_add_db(opt.ns()?, opt.db()?, opt.strict).await?; txn.set( diff --git a/core/src/sql/statements/mod.rs b/core/src/sql/statements/mod.rs index 08f21629..49e8f031 100644 --- a/core/src/sql/statements/mod.rs +++ b/core/src/sql/statements/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod access; pub(crate) mod alter; pub(crate) mod analyze; pub(crate) mod begin; @@ -28,6 +29,9 @@ pub(crate) mod update; pub(crate) mod upsert; pub(crate) mod r#use; +// TODO(gguillemas): Document once bearer access is no longer experimental. +#[doc(hidden)] +pub use self::access::{AccessGrant, AccessStatement}; pub use self::analyze::AnalyzeStatement; pub use self::begin::BeginStatement; pub use self::cancel::CancelStatement; diff --git a/core/src/sql/statements/remove/access.rs b/core/src/sql/statements/remove/access.rs index d5c79cc3..0e80434b 100644 --- a/core/src/sql/statements/remove/access.rs +++ b/core/src/sql/statements/remove/access.rs @@ -33,8 +33,11 @@ impl RemoveAccessStatement { // Get the definition let ac = txn.get_root_access(&self.name).await?; // Delete the definition - let key = crate::key::root::ac::new(&ac.name); + let key = crate::key::root::access::ac::new(&ac.name); txn.del(key).await?; + // Delete any associated data including access grants. + let key = crate::key::root::access::all::new(&ac.name); + txn.delp(key).await?; // Clear the cache txn.clear(); // Ok all good @@ -46,8 +49,11 @@ impl RemoveAccessStatement { // Get the definition let ac = txn.get_ns_access(opt.ns()?, &self.name).await?; // Delete the definition - let key = crate::key::namespace::ac::new(opt.ns()?, &ac.name); + let key = crate::key::namespace::access::ac::new(opt.ns()?, &ac.name); txn.del(key).await?; + // Delete any associated data including access grants. + let key = crate::key::namespace::access::all::new(opt.ns()?, &ac.name); + txn.delp(key).await?; // Clear the cache txn.clear(); // Ok all good @@ -59,8 +65,12 @@ impl RemoveAccessStatement { // Get the definition let ac = txn.get_db_access(opt.ns()?, opt.db()?, &self.name).await?; // Delete the definition - let key = crate::key::database::ac::new(opt.ns()?, opt.db()?, &ac.name); + let key = crate::key::database::access::ac::new(opt.ns()?, opt.db()?, &ac.name); txn.del(key).await?; + // Delete any associated data including access grants. + let key = + crate::key::database::access::all::new(opt.ns()?, opt.db()?, &ac.name); + txn.delp(key).await?; // Clear the cache txn.clear(); // Ok all good diff --git a/core/src/sql/value/value.rs b/core/src/sql/value/value.rs index 456be10f..1a9f223e 100644 --- a/core/src/sql/value/value.rs +++ b/core/src/sql/value/value.rs @@ -542,6 +542,15 @@ impl From> for Value { } } +impl From> for Value { + fn from(v: Option) -> Self { + match v { + Some(v) => Value::from(v), + None => Value::None, + } + } +} + impl From for Value { fn from(v: Id) -> Self { match v { diff --git a/core/src/syn/lexer/keywords.rs b/core/src/syn/lexer/keywords.rs index 73c32bf1..47639989 100644 --- a/core/src/syn/lexer/keywords.rs +++ b/core/src/syn/lexer/keywords.rs @@ -70,6 +70,7 @@ pub(crate) static KEYWORDS: phf::Map, TokenKind> = phf_map UniCase::ascii("ASSERT") => TokenKind::Keyword(Keyword::Assert), UniCase::ascii("AT") => TokenKind::Keyword(Keyword::At), UniCase::ascii("AUTHENTICATE") => TokenKind::Keyword(Keyword::Authenticate), + UniCase::ascii("BEARER") => TokenKind::Keyword(Keyword::Bearer), UniCase::ascii("BEFORE") => TokenKind::Keyword(Keyword::Before), UniCase::ascii("BEGIN") => TokenKind::Keyword(Keyword::Begin), UniCase::ascii("BLANK") => TokenKind::Keyword(Keyword::Blank), @@ -145,6 +146,7 @@ pub(crate) static KEYWORDS: phf::Map, TokenKind> = phf_map UniCase::ascii("KILL") => TokenKind::Keyword(Keyword::Kill), UniCase::ascii("LET") => TokenKind::Keyword(Keyword::Let), UniCase::ascii("LIMIT") => TokenKind::Keyword(Keyword::Limit), + UniCase::ascii("LIST") => TokenKind::Keyword(Keyword::List), UniCase::ascii("LIVE") => TokenKind::Keyword(Keyword::Live), UniCase::ascii("LOWERCASE") => TokenKind::Keyword(Keyword::Lowercase), UniCase::ascii("LM") => TokenKind::Keyword(Keyword::Lm), @@ -178,6 +180,7 @@ pub(crate) static KEYWORDS: phf::Map, TokenKind> = phf_map UniCase::ascii("PERMISSIONS") => TokenKind::Keyword(Keyword::Permissions), UniCase::ascii("POSTINGS_CACHE") => TokenKind::Keyword(Keyword::PostingsCache), UniCase::ascii("POSTINGS_ORDER") => TokenKind::Keyword(Keyword::PostingsOrder), + UniCase::ascii("PRUNE") => TokenKind::Keyword(Keyword::Prune), UniCase::ascii("PUNCT") => TokenKind::Keyword(Keyword::Punct), UniCase::ascii("READONLY") => TokenKind::Keyword(Keyword::Readonly), UniCase::ascii("RELATE") => TokenKind::Keyword(Keyword::Relate), @@ -186,6 +189,7 @@ pub(crate) static KEYWORDS: phf::Map, TokenKind> = phf_map UniCase::ascii("REMOVE") => TokenKind::Keyword(Keyword::Remove), UniCase::ascii("REPLACE") => TokenKind::Keyword(Keyword::Replace), UniCase::ascii("RETURN") => TokenKind::Keyword(Keyword::Return), + UniCase::ascii("REVOKE") => TokenKind::Keyword(Keyword::Revoke), UniCase::ascii("ROLES") => TokenKind::Keyword(Keyword::Roles), UniCase::ascii("ROOT") => TokenKind::Keyword(Keyword::Root), UniCase::ascii("KV") => TokenKind::Keyword(Keyword::Root), diff --git a/core/src/syn/parser/stmt/define.rs b/core/src/syn/parser/stmt/define.rs index 1add0c91..7a2c3228 100644 --- a/core/src/syn/parser/stmt/define.rs +++ b/core/src/syn/parser/stmt/define.rs @@ -1,5 +1,6 @@ use reblessive::Stk; +use crate::cnf::EXPERIMENTAL_BEARER_ACCESS; use crate::sql::access_type::JwtAccessVerify; use crate::sql::index::HnswParams; use crate::{ @@ -339,6 +340,25 @@ impl Parser<'_> { } res.kind = AccessType::Record(ac); } + t!("BEARER") => { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + if !*EXPERIMENTAL_BEARER_ACCESS { + unexpected!( + self, + t!("BEARER"), + "the experimental bearer access feature to be enabled" + ); + } + self.pop_peek(); + let mut ac = access_type::BearerAccess { + ..Default::default() + }; + if self.eat(t!("WITH")) { + expected!(self, t!("JWT")); + ac.jwt = self.parse_jwt()?; + } + res.kind = AccessType::Bearer(ac); + } _ => break, } } diff --git a/core/src/syn/parser/stmt/mod.rs b/core/src/syn/parser/stmt/mod.rs index 1ef12f9e..dd2e51cc 100644 --- a/core/src/syn/parser/stmt/mod.rs +++ b/core/src/syn/parser/stmt/mod.rs @@ -1,11 +1,15 @@ use reblessive::Stk; +use crate::cnf::EXPERIMENTAL_BEARER_ACCESS; use crate::enter_query_recursion; use crate::sql::block::Entry; use crate::sql::statements::rebuild::{RebuildIndexStatement, RebuildStatement}; use crate::sql::statements::show::{ShowSince, ShowStatement}; use crate::sql::statements::sleep::SleepStatement; use crate::sql::statements::{ + access::{ + AccessStatement, AccessStatementGrant, AccessStatementList, AccessStatementRevoke, Subject, + }, KillStatement, LiveStatement, OptionStatement, SetStatement, ThrowStatement, }; use crate::sql::{Fields, Ident, Param}; @@ -81,21 +85,22 @@ impl Parser<'_> { fn token_kind_starts_statement(kind: TokenKind) -> bool { matches!( kind, - t!("ALTER") - | t!("ANALYZE") | t!("BEGIN") - | t!("BREAK") | t!("CANCEL") - | t!("COMMIT") | t!("CONTINUE") - | t!("CREATE") | t!("DEFINE") - | t!("DELETE") | t!("FOR") - | t!("IF") | t!("INFO") - | t!("INSERT") | t!("KILL") - | t!("LIVE") | t!("OPTION") - | t!("REBUILD") | t!("RETURN") - | t!("RELATE") | t!("REMOVE") - | t!("SELECT") | t!("LET") - | t!("SHOW") | t!("SLEEP") - | t!("THROW") | t!("UPDATE") - | t!("UPSERT") | t!("USE") + t!("ACCESS") + | t!("ALTER") | t!("ANALYZE") + | t!("BEGIN") | t!("BREAK") + | t!("CANCEL") | t!("COMMIT") + | t!("CONTINUE") | t!("CREATE") + | t!("DEFINE") | t!("DELETE") + | t!("FOR") | t!("IF") + | t!("INFO") | t!("INSERT") + | t!("KILL") | t!("LIVE") + | t!("OPTION") | t!("REBUILD") + | t!("RETURN") | t!("RELATE") + | t!("REMOVE") | t!("SELECT") + | t!("LET") | t!("SHOW") + | t!("SLEEP") | t!("THROW") + | t!("UPDATE") | t!("UPSERT") + | t!("USE") ) } @@ -108,6 +113,18 @@ impl Parser<'_> { async fn parse_stmt_inner(&mut self, ctx: &mut Stk) -> ParseResult { let token = self.peek(); match token.kind { + t!("ACCESS") => { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + if !*EXPERIMENTAL_BEARER_ACCESS { + unexpected!( + self, + t!("ACCESS"), + "the experimental bearer access feature to be enabled" + ); + } + self.pop_peek(); + self.parse_access().map(Statement::Access) + } t!("ALTER") => { self.pop_peek(); ctx.run(|ctx| self.parse_alter_stmt(ctx)).await.map(Statement::Alter) @@ -362,6 +379,45 @@ impl Parser<'_> { } } + /// Parsers an access statement. + fn parse_access(&mut self) -> ParseResult { + let ac = self.next_token_value()?; + let base = self.eat(t!("ON")).then(|| self.parse_base(false)).transpose()?; + match self.peek_kind() { + t!("GRANT") => { + self.pop_peek(); + // TODO(gguillemas): Implement rest of the syntax. + expected!(self, t!("FOR")); + expected!(self, t!("USER")); + let user = self.next_token_value()?; + Ok(AccessStatement::Grant(AccessStatementGrant { + ac, + base, + subject: Some(Subject::User(user)), + })) + } + t!("LIST") => { + self.pop_peek(); + // TODO(gguillemas): Implement rest of the syntax. + Ok(AccessStatement::List(AccessStatementList { + ac, + base, + })) + } + t!("REVOKE") => { + self.pop_peek(); + let gr = self.next_token_value()?; + Ok(AccessStatement::Revoke(AccessStatementRevoke { + ac, + base, + gr, + })) + } + // TODO(gguillemas): Implement rest of the statements. + x => unexpected!(self, x, "an implemented statement"), + } + } + /// Parsers a analyze statement. fn parse_analyze(&mut self) -> ParseResult { expected!(self, t!("INDEX")); diff --git a/core/src/syn/parser/test/stmt.rs b/core/src/syn/parser/test/stmt.rs index 50e1392e..0e3d8320 100644 --- a/core/src/syn/parser/test/stmt.rs +++ b/core/src/syn/parser/test/stmt.rs @@ -11,11 +11,13 @@ use crate::{ index::{Distance, HnswParams, MTreeParams, SearchParams, VectorType}, language::Language, statements::{ + access, + access::{AccessStatementGrant, AccessStatementList, AccessStatementRevoke}, analyze::AnalyzeStatement, show::{ShowSince, ShowStatement}, sleep::SleepStatement, - BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement, - CreateStatement, DefineAccessStatement, DefineAnalyzerStatement, + AccessStatement, BeginStatement, BreakStatement, CancelStatement, CommitStatement, + ContinueStatement, CreateStatement, DefineAccessStatement, DefineAnalyzerStatement, DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement, DefineFunctionStatement, DefineIndexStatement, DefineNamespaceStatement, DefineParamStatement, DefineStatement, DefineTableStatement, DeleteStatement, @@ -2419,3 +2421,41 @@ fn parse_upsert() { }) ); } + +#[test] +fn parse_access_grant() { + let res = test_parse!(parse_stmt, r#"ACCESS a ON NAMESPACE GRANT FOR USER b"#).unwrap(); + assert_eq!( + res, + Statement::Access(AccessStatement::Grant(AccessStatementGrant { + ac: Ident("a".to_string()), + base: Some(Base::Ns), + subject: Some(access::Subject::User(Ident("b".to_string()))), + })) + ); +} + +#[test] +fn parse_access_revoke() { + let res = test_parse!(parse_stmt, r#"ACCESS a ON DATABASE REVOKE b"#).unwrap(); + assert_eq!( + res, + Statement::Access(AccessStatement::Revoke(AccessStatementRevoke { + ac: Ident("a".to_string()), + base: Some(Base::Db), + gr: Ident("b".to_string()), + })) + ); +} + +#[test] +fn parse_access_list() { + let res = test_parse!(parse_stmt, r#"ACCESS a LIST"#).unwrap(); + assert_eq!( + res, + Statement::Access(AccessStatement::List(AccessStatementList { + ac: Ident("a".to_string()), + base: None, + })) + ); +} diff --git a/core/src/syn/token/keyword.rs b/core/src/syn/token/keyword.rs index eb61f106..dfcbe376 100644 --- a/core/src/syn/token/keyword.rs +++ b/core/src/syn/token/keyword.rs @@ -37,6 +37,7 @@ keyword! { Assert => "ASSERT", At => "AT", Authenticate => "AUTHENTICATE", + Bearer => "BEARER", Before => "BEFORE", Begin => "BEGIN", Blank => "BLANK", @@ -106,6 +107,7 @@ keyword! { Kill => "KILL", Let => "LET", Limit => "LIMIT", + List => "LIST", Live => "LIVE", Lowercase => "LOWERCASE", Lm => "LM", @@ -137,6 +139,7 @@ keyword! { Permissions => "PERMISSIONS", PostingsCache => "POSTINGS_CACHE", PostingsOrder => "POSTINGS_ORDER", + Prune => "PRUNE", Punct => "PUNCT", Readonly => "READONLY", Rebuild => "REBUILD", @@ -145,6 +148,7 @@ keyword! { Remove => "REMOVE", Replace => "REPLACE", Return => "RETURN", + Revoke => "REVOKE", Roles => "ROLES", Root => "ROOT", Schemafull => "SCHEMAFULL", diff --git a/lib/tests/access.rs b/lib/tests/access.rs new file mode 100644 index 00000000..45c8df5f --- /dev/null +++ b/lib/tests/access.rs @@ -0,0 +1,629 @@ +mod parse; +use parse::Parse; +mod helpers; +use helpers::new_ds; +use regex::Regex; +use surrealdb::dbs::Session; +use surrealdb::iam::Role; +use surrealdb::sql::Value; + +#[tokio::test] +async fn access_bearer_database() -> () { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + std::env::set_var("SURREAL_EXPERIMENTAL_BEARER_ACCESS", "true"); + + let sql = " + -- Initial setup + DEFINE ACCESS api ON DATABASE TYPE BEARER; + DEFINE USER tobie ON DATABASE PASSWORD 'secret' ROLES EDITOR; + INFO FOR DB; + -- Should succeed + ACCESS api ON DATABASE GRANT FOR USER tobie; + ACCESS api ON DATABASE GRANT FOR USER tobie; + ACCESS api GRANT FOR USER tobie; + ACCESS api GRANT FOR USER tobie; + ACCESS api ON DATABASE LIST; + ACCESS api LIST; + -- Should fail + ACCESS invalid ON DATABASE GRANT FOR USER tobie; + ACCESS invalid GRANT FOR USER tobie; + ACCESS api ON DATABASE GRANT FOR USER invalid; + ACCESS api GRANT FOR USER invalid; + ACCESS invalid ON DATABASE LIST; + ACCESS invalid LIST; + "; + let dbs = new_ds().await.unwrap(); + let ses = Session::owner().with_ns("test").with_db("test"); + let res = &mut dbs.execute(sql, &ses, None).await.unwrap(); + assert_eq!(res.len(), 15); + // Consume the results of the setup statements + res.remove(0).result.unwrap(); + res.remove(0).result.unwrap(); + // Ensure the access method was created as expected + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ accesses: \{ api: 'DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR GRANT NONE, FOR TOKEN 1h, FOR SESSION NONE' \}, .* \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = Regex::new( + r"\[\{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}\]", + ) + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = Regex::new( + r"\[\{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}\]", + ) + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!( + tmp.to_string(), + "The access method 'invalid' does not exist in the database 'test'" + ); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!( + tmp.to_string(), + "The access method 'invalid' does not exist in the database 'test'" + ); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "The user 'invalid' does not exist in the database 'test'"); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "The user 'invalid' does not exist in the database 'test'"); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!( + tmp.to_string(), + "The access method 'invalid' does not exist in the database 'test'" + ); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!( + tmp.to_string(), + "The access method 'invalid' does not exist in the database 'test'" + ); +} + +#[tokio::test] +async fn access_bearer_namespace() { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + std::env::set_var("SURREAL_EXPERIMENTAL_BEARER_ACCESS", "true"); + + let sql = " + -- Initial setup + DEFINE ACCESS api ON NAMESPACE TYPE BEARER; + DEFINE USER tobie ON NAMESPACE PASSWORD 'secret' ROLES EDITOR; + INFO FOR NS; + -- Should succeed + ACCESS api ON NAMESPACE GRANT FOR USER tobie; + ACCESS api ON NAMESPACE GRANT FOR USER tobie; + ACCESS api GRANT FOR USER tobie; + ACCESS api GRANT FOR USER tobie; + ACCESS api ON NAMESPACE LIST; + ACCESS api LIST; + -- Should fail + ACCESS invalid ON NAMESPACE GRANT FOR USER tobie; + ACCESS invalid GRANT FOR USER tobie; + ACCESS api ON NAMESPACE GRANT FOR USER invalid; + ACCESS api GRANT FOR USER invalid; + ACCESS invalid ON NAMESPACE LIST; + ACCESS invalid LIST; + "; + let dbs = new_ds().await.unwrap(); + let ses = Session::owner().with_ns("test"); + let res = &mut dbs.execute(sql, &ses, None).await.unwrap(); + assert_eq!(res.len(), 15); + // Consume the results of the setup statements + res.remove(0).result.unwrap(); + res.remove(0).result.unwrap(); + // Ensure the access method was created as expected + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ accesses: \{ api: 'DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR GRANT NONE, FOR TOKEN 1h, FOR SESSION NONE' \}, .* \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = Regex::new( + r"\[\{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}\]", + ) + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = Regex::new( + r"\[\{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}\]", + ) + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!( + tmp.to_string(), + "The access method 'invalid' does not exist in the namespace 'test'" + ); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!( + tmp.to_string(), + "The access method 'invalid' does not exist in the namespace 'test'" + ); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "The user 'invalid' does not exist in the namespace 'test'"); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "The user 'invalid' does not exist in the namespace 'test'"); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!( + tmp.to_string(), + "The access method 'invalid' does not exist in the namespace 'test'" + ); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!( + tmp.to_string(), + "The access method 'invalid' does not exist in the namespace 'test'" + ); +} + +#[tokio::test] +async fn access_bearer_root() { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + std::env::set_var("SURREAL_EXPERIMENTAL_BEARER_ACCESS", "true"); + + let sql = " + -- Initial setup + DEFINE ACCESS api ON ROOT TYPE BEARER; + DEFINE USER tobie ON ROOT PASSWORD 'secret' ROLES EDITOR; + INFO FOR ROOT; + -- Should succeed + ACCESS api ON ROOT GRANT FOR USER tobie; + ACCESS api ON ROOT GRANT FOR USER tobie; + ACCESS api GRANT FOR USER tobie; + ACCESS api GRANT FOR USER tobie; + ACCESS api ON ROOT LIST; + ACCESS api LIST; + -- Should fail + ACCESS invalid ON ROOT GRANT FOR USER tobie; + ACCESS invalid GRANT FOR USER tobie; + ACCESS api ON ROOT GRANT FOR USER invalid; + ACCESS api GRANT FOR USER invalid; + ACCESS invalid ON ROOT LIST; + ACCESS invalid LIST; + "; + let dbs = new_ds().await.unwrap(); + let ses = Session::owner(); + let res = &mut dbs.execute(sql, &ses, None).await.unwrap(); + assert_eq!(res.len(), 15); + // Consume the results of the setup statements + res.remove(0).result.unwrap(); + res.remove(0).result.unwrap(); + // Ensure the access method was created as expected + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ accesses: \{ api: 'DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR GRANT NONE, FOR TOKEN 1h, FOR SESSION NONE' \}, .* \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = Regex::new( + r"\[\{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}\]", + + ) + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = Regex::new( + r"\[\{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}, \{ ac: 'api', .*, grant: \{ id: .*, key: '\[REDACTED\]' \}, .* \}\]", + ) + .unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "The root access method 'invalid' does not exist"); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "The root access method 'invalid' does not exist"); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "The root user 'invalid' does not exist"); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "The root user 'invalid' does not exist"); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "The root access method 'invalid' does not exist"); + // + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "The root access method 'invalid' does not exist"); +} + +#[tokio::test] +async fn access_bearer_revoke_db() -> () { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + std::env::set_var("SURREAL_EXPERIMENTAL_BEARER_ACCESS", "true"); + + let sql = " + -- Initial setup + DEFINE ACCESS api ON DATABASE TYPE BEARER; + DEFINE USER tobie ON DATABASE PASSWORD 'secret' ROLES EDITOR; + ACCESS api ON DATABASE GRANT FOR USER tobie; + "; + let dbs = new_ds().await.unwrap(); + let ses = Session::owner().with_ns("test").with_db("test"); + let res = &mut dbs.execute(sql, &ses, None).await.unwrap(); + // Consume the results of the setup statements + res.remove(0).result.unwrap(); + res.remove(0).result.unwrap(); + // Retrieve the generated bearer key + let tmp = res.remove(0).result.unwrap().to_string(); + let re = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ id: '(.*)', key: .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + let kid = re.captures(&tmp).unwrap().get(1).unwrap().as_str(); + // Revoke bearer key + let res = &mut dbs.execute(&format!("ACCESS api REVOKE `{kid}`"), &ses, None).await.unwrap(); + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = Regex::new(r"\{ ac: 'api', .*, revocation: d'.*', .* \}").unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // Attempt to revoke bearer key again + let res = &mut dbs.execute(&format!("ACCESS api REVOKE `{kid}`"), &ses, None).await.unwrap(); + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "This access grant has been revoked"); +} + +#[tokio::test] +async fn access_bearer_revoke_ns() -> () { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + std::env::set_var("SURREAL_EXPERIMENTAL_BEARER_ACCESS", "true"); + + let sql = " + -- Initial setup + DEFINE ACCESS api ON NAMESPACE TYPE BEARER; + DEFINE USER tobie ON NAMESPACE PASSWORD 'secret' ROLES EDITOR; + ACCESS api ON NAMESPACE GRANT FOR USER tobie; + "; + let dbs = new_ds().await.unwrap(); + let ses = Session::owner().with_ns("test"); + let res = &mut dbs.execute(sql, &ses, None).await.unwrap(); + // Consume the results of the setup statements + res.remove(0).result.unwrap(); + res.remove(0).result.unwrap(); + // Retrieve the generated bearer key + let tmp = res.remove(0).result.unwrap().to_string(); + let re = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ id: '(.*)', key: .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + let kid = re.captures(&tmp).unwrap().get(1).unwrap().as_str(); + // Revoke bearer key + let res = &mut dbs.execute(&format!("ACCESS api REVOKE `{kid}`"), &ses, None).await.unwrap(); + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = Regex::new(r"\{ ac: 'api', .*, revocation: d'.*', .* \}").unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // Attempt to revoke bearer key again + let res = &mut dbs.execute(&format!("ACCESS api REVOKE `{kid}`"), &ses, None).await.unwrap(); + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "This access grant has been revoked"); +} + +#[tokio::test] +async fn access_bearer_revoke_root() -> () { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + std::env::set_var("SURREAL_EXPERIMENTAL_BEARER_ACCESS", "true"); + + let sql = " + -- Initial setup + DEFINE ACCESS api ON ROOT TYPE BEARER; + DEFINE USER tobie ON ROOT PASSWORD 'secret' ROLES EDITOR; + ACCESS api ON ROOT GRANT FOR USER tobie; + "; + let dbs = new_ds().await.unwrap(); + let ses = Session::owner(); + let res = &mut dbs.execute(sql, &ses, None).await.unwrap(); + // Consume the results of the setup statements + res.remove(0).result.unwrap(); + res.remove(0).result.unwrap(); + // Retrieve the generated bearer key + let tmp = res.remove(0).result.unwrap().to_string(); + let re = + Regex::new(r"\{ ac: 'api', creation: .*, expiration: NONE, grant: \{ id: '(.*)', key: .* \}, id: .*, revocation: NONE, subject: \{ user: 'tobie' \} \}") + .unwrap(); + let kid = re.captures(&tmp).unwrap().get(1).unwrap().as_str(); + // Revoke bearer key + let res = &mut dbs.execute(&format!("ACCESS api REVOKE `{kid}`"), &ses, None).await.unwrap(); + let tmp = res.remove(0).result.unwrap().to_string(); + let ok = Regex::new(r"\{ ac: 'api', .*, revocation: d'.*', .* \}").unwrap(); + assert!(ok.is_match(&tmp), "Output '{}' doesn't match regex '{}'", tmp, ok); + // Attempt to revoke bearer key again + let res = &mut dbs.execute(&format!("ACCESS api REVOKE `{kid}`"), &ses, None).await.unwrap(); + let tmp = res.remove(0).result.unwrap_err(); + assert_eq!(tmp.to_string(), "This access grant has been revoked"); +} + +// +// Permissions +// + +#[tokio::test] +async fn permissions_access_grant_db() { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + std::env::set_var("SURREAL_EXPERIMENTAL_BEARER_ACCESS", "true"); + + let tests = vec![ + // Root level + ((().into(), Role::Owner), ("NS", "DB"), true, "owner at root level should be able to issue a grant"), + ((().into(), Role::Editor), ("NS", "DB"), false, "editor at root level should not be able to issue a grant"), + ((().into(), Role::Viewer), ("NS", "DB"), false, "viewer at root level should not be able to issue a grant"), + + // Namespace level + ((("NS",).into(), Role::Owner), ("NS", "DB"), true, "owner at namespace level should be able to issue a grant on its namespace"), + ((("NS",).into(), Role::Owner), ("OTHER_NS", "DB"), false, "owner at namespace level should not be able to issue a grant on another namespace"), + ((("NS",).into(), Role::Editor), ("NS", "DB"), false, "editor at namespace level should not be able to issue a grant on its namespace"), + ((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false, "editor at namespace level should not be able to issue a grant on another namespace"), + ((("NS",).into(), Role::Viewer), ("NS", "DB"), false, "viewer at namespace level should not be able to issue a grant on its namespace"), + ((("NS",).into(), Role::Viewer), ("OTHER_NS", "DB"), false, "viewer at namespace level should not be able to issue a grant on another namespace"), + + // Database level + ((("NS", "DB").into(), Role::Owner), ("NS", "DB"), true, "owner at database level should be able to issue a grant on its database"), + ((("NS", "DB").into(), Role::Owner), ("NS", "OTHER_DB"), false, "owner at database level should not be able to issue a grant on another database"), + ((("NS", "DB").into(), Role::Owner), ("OTHER_NS", "DB"), false, "owner at database level should not be able to issue a grant on another namespace even if the database name matches"), + ((("NS", "DB").into(), Role::Editor), ("NS", "DB"), false, "editor at database level should not be able to issue a grant on its database"), + ((("NS", "DB").into(), Role::Editor), ("NS", "OTHER_DB"), false, "editor at database level should not be able to issue a grant on another database"), + ((("NS", "DB").into(), Role::Editor), ("OTHER_NS", "DB"), false, "editor at database level should not be able to issue a grant on another namespace even if the database name matches"), + ((("NS", "DB").into(), Role::Viewer), ("NS", "DB"), false, "viewer at database level should not be able to issue a grant on its database"), + ((("NS", "DB").into(), Role::Viewer), ("NS", "OTHER_DB"), false, "viewer at database level should not be able to issue a grant on another database"), + ((("NS", "DB").into(), Role::Viewer), ("OTHER_NS", "DB"), false, "viewer at database level should not be able to issue a grant on another namespace even if the database name matches"), + ]; + let statement = "ACCESS api ON DATABASE GRANT FOR USER tobie"; + + for ((level, role), (ns, db), should_succeed, msg) in tests.into_iter() { + let sess = Session::for_level(level, role).with_ns(ns).with_db(db); + + let sess_setup = + Session::for_level(("NS", "DB").into(), Role::Owner).with_ns("NS").with_db("DB"); + let statement_setup = + "DEFINE ACCESS api ON DATABASE TYPE BEARER; DEFINE USER tobie ON DATABASE ROLES OWNER"; + + { + let ds = new_ds().await.unwrap().with_auth_enabled(true); + + let mut resp = ds.execute(&statement_setup, &sess_setup, None).await.unwrap(); + let res = resp.remove(0).output(); + assert!(res.is_ok(), "Error setting up access method: {:?}", res); + let res = resp.remove(0).output(); + assert!(res.is_ok(), "Error setting up user: {:?}", res); + + let mut resp = ds.execute(statement, &sess, None).await.unwrap(); + let res = resp.remove(0).output(); + + if should_succeed { + assert!(res.is_ok(), "{}: {:?}", msg, res); + assert_ne!(res.unwrap(), Value::parse("[]"), "{}", msg); + } else { + let err = res.unwrap_err().to_string(); + assert!( + err.contains("Not enough permissions to perform this action"), + "{}: {}", + msg, + err + ) + } + } + } +} + +#[tokio::test] +async fn permissions_access_grant_ns() { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + std::env::set_var("SURREAL_EXPERIMENTAL_BEARER_ACCESS", "true"); + + let tests = vec![ + // Root level + ((().into(), Role::Owner), ("NS", "DB"), true, "owner at root level should be able to issue a grant"), + ((().into(), Role::Editor), ("NS", "DB"), false, "editor at root level should not be able to issue a grant"), + ((().into(), Role::Viewer), ("NS", "DB"), false, "viewer at root level should not be able to issue a grant"), + + // Namespace level + ((("NS",).into(), Role::Owner), ("NS", "DB"), true, "owner at namespace level should be able to issue a grant on its namespace"), + ((("NS",).into(), Role::Owner), ("OTHER_NS", "DB"), false, "owner at namespace level should not be able to issue a grant on another namespace"), + ((("NS",).into(), Role::Editor), ("NS", "DB"), false, "editor at namespace level should not be able to issue a grant on its namespace"), + ((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false, "editor at namespace level should not be able to issue a grant on another namespace"), + ((("NS",).into(), Role::Viewer), ("NS", "DB"), false, "viewer at namespace level should not be able to issue a grant on its namespace"), + ((("NS",).into(), Role::Viewer), ("OTHER_NS", "DB"), false, "viewer at namespace level should not be able to issue a grant on another namespace"), + + // Database level + ((("NS", "DB").into(), Role::Owner), ("NS", "DB"), false, "owner at database level should not be able to issue a grant on its database"), + ((("NS", "DB").into(), Role::Owner), ("NS", "OTHER_DB"), false, "owner at database level should not be able to issue a grant on another database"), + ((("NS", "DB").into(), Role::Owner), ("OTHER_NS", "DB"), false, "owner at database level should not be able to issue a grant on another namespace even if the database name matches"), + ((("NS", "DB").into(), Role::Editor), ("NS", "DB"), false, "editor at database level should not be able to issue a grant on its database"), + ((("NS", "DB").into(), Role::Editor), ("NS", "OTHER_DB"), false, "editor at database level should not be able to issue a grant on another database"), + ((("NS", "DB").into(), Role::Editor), ("OTHER_NS", "DB"), false, "editor at database level should not be able to issue a grant on another namespace even if the database name matches"), + ((("NS", "DB").into(), Role::Viewer), ("NS", "DB"), false, "viewer at database level should not be able to issue a grant on its database"), + ((("NS", "DB").into(), Role::Viewer), ("NS", "OTHER_DB"), false, "viewer at database level should not be able to issue a grant on another database"), + ((("NS", "DB").into(), Role::Viewer), ("OTHER_NS", "DB"), false, "viewer at database level should not be able to issue a grant on another namespace even if the database name matches"), + ]; + let statement = "ACCESS api ON NAMESPACE GRANT FOR USER tobie"; + + for ((level, role), (ns, db), should_succeed, msg) in tests.into_iter() { + let sess = Session::for_level(level, role).with_ns(ns).with_db(db); + + let sess_setup = + Session::for_level(("NS",).into(), Role::Owner).with_ns("NS").with_db("DB"); + let statement_setup = "DEFINE ACCESS api ON NAMESPACE TYPE BEARER; DEFINE USER tobie ON NAMESPACE ROLES OWNER"; + + { + let ds = new_ds().await.unwrap().with_auth_enabled(true); + + let mut resp = ds.execute(&statement_setup, &sess_setup, None).await.unwrap(); + let res = resp.remove(0).output(); + assert!(res.is_ok(), "Error setting up access method: {:?}", res); + let res = resp.remove(0).output(); + assert!(res.is_ok(), "Error setting up user: {:?}", res); + + let mut resp = ds.execute(statement, &sess, None).await.unwrap(); + let res = resp.remove(0).output(); + + if should_succeed { + assert!(res.is_ok(), "{}: {:?}", msg, res); + assert_ne!(res.unwrap(), Value::parse("[]"), "{}", msg); + } else { + let err = res.unwrap_err().to_string(); + assert!( + err.contains("Not enough permissions to perform this action"), + "{}: {}", + msg, + err + ) + } + } + } +} + +#[tokio::test] +async fn permissions_access_grant_root() { + // TODO(gguillemas): Remove this once bearer access is no longer experimental. + std::env::set_var("SURREAL_EXPERIMENTAL_BEARER_ACCESS", "true"); + + let tests = vec![ + // Root level + ((().into(), Role::Owner), ("NS", "DB"), true, "owner at root level should be able to issue a grant"), + ((().into(), Role::Editor), ("NS", "DB"), false, "editor at root level should not be able to issue a grant"), + ((().into(), Role::Viewer), ("NS", "DB"), false, "viewer at root level should not be able to issue a grant"), + + // Namespace level + ((("NS",).into(), Role::Owner), ("NS", "DB"), false, "owner at namespace level should not be able to issue a grant on its namespace"), + ((("NS",).into(), Role::Owner), ("OTHER_NS", "DB"), false, "owner at namespace level should not be able to issue a grant on another namespace"), + ((("NS",).into(), Role::Editor), ("NS", "DB"), false, "editor at namespace level should not be able to issue a grant on its namespace"), + ((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false, "editor at namespace level should not be able to issue a grant on another namespace"), + ((("NS",).into(), Role::Viewer), ("NS", "DB"), false, "viewer at namespace level should not be able to issue a grant on its namespace"), + ((("NS",).into(), Role::Viewer), ("OTHER_NS", "DB"), false, "viewer at namespace level should not be able to issue a grant on another namespace"), + + // Database level + ((("NS", "DB").into(), Role::Owner), ("NS", "DB"), false, "owner at database level should not be able to issue a grant on its database"), + ((("NS", "DB").into(), Role::Owner), ("NS", "OTHER_DB"), false, "owner at database level should not be able to issue a grant on another database"), + ((("NS", "DB").into(), Role::Owner), ("OTHER_NS", "DB"), false, "owner at database level should not be able to issue a grant on another namespace even if the database name matches"), + ((("NS", "DB").into(), Role::Editor), ("NS", "DB"), false, "editor at database level should not be able to issue a grant on its database"), + ((("NS", "DB").into(), Role::Editor), ("NS", "OTHER_DB"), false, "editor at database level should not be able to issue a grant on another database"), + ((("NS", "DB").into(), Role::Editor), ("OTHER_NS", "DB"), false, "editor at database level should not be able to issue a grant on another namespace even if the database name matches"), + ((("NS", "DB").into(), Role::Viewer), ("NS", "DB"), false, "viewer at database level should not be able to issue a grant on its database"), + ((("NS", "DB").into(), Role::Viewer), ("NS", "OTHER_DB"), false, "viewer at database level should not be able to issue a grant on another database"), + ((("NS", "DB").into(), Role::Viewer), ("OTHER_NS", "DB"), false, "viewer at database level should not be able to issue a grant on another namespace even if the database name matches"), + ]; + let statement = "ACCESS api ON ROOT GRANT FOR USER tobie"; + + for ((level, role), (ns, db), should_succeed, msg) in tests.into_iter() { + let sess = Session::for_level(level, role).with_ns(ns).with_db(db); + + let sess_setup = Session::for_level(().into(), Role::Owner).with_ns("NS").with_db("DB"); + let statement_setup = + "DEFINE ACCESS api ON ROOT TYPE BEARER; DEFINE USER tobie ON ROOT ROLES OWNER"; + + { + let ds = new_ds().await.unwrap().with_auth_enabled(true); + + let mut resp = ds.execute(&statement_setup, &sess_setup, None).await.unwrap(); + let res = resp.remove(0).output(); + assert!(res.is_ok(), "Error setting up access method: {:?}", res); + let res = resp.remove(0).output(); + assert!(res.is_ok(), "Error setting up user: {:?}", res); + + let mut resp = ds.execute(statement, &sess, None).await.unwrap(); + let res = resp.remove(0).output(); + + if should_succeed { + assert!(res.is_ok(), "{}: {:?}", msg, res); + assert_ne!(res.unwrap(), Value::parse("[]"), "{}", msg); + } else { + let err = res.unwrap_err().to_string(); + assert!( + err.contains("Not enough permissions to perform this action"), + "{}: {}", + msg, + err + ) + } + } + } +}