Add BEARER access type and its basic grant management (#4302)

Co-authored-by: Emmanuel Keller <emmanuel.keller@surrealdb.com>
Co-authored-by: Micha de Vries <micha@devrie.sh>
This commit is contained in:
Gerard Guillemas Martos 2024-08-13 18:38:17 +02:00 committed by GitHub
parent a87433c4d3
commit c3d788ff4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 4928 additions and 170 deletions

5
Cargo.lock generated
View file

@ -5773,9 +5773,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.5.0" version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]] [[package]]
name = "surreal" name = "surreal"
@ -5980,6 +5980,7 @@ dependencies = [
"sha2", "sha2",
"snap", "snap",
"storekey", "storekey",
"subtle",
"surrealdb-derive", "surrealdb-derive",
"surrealdb-tikv-client", "surrealdb-tikv-client",
"surrealkv", "surrealkv",

View file

@ -142,6 +142,7 @@ sha1 = "0.10.6"
sha2 = "0.10.8" sha2 = "0.10.8"
snap = "1.1.0" snap = "1.1.0"
storekey = "0.5.0" storekey = "0.5.0"
subtle = "2.6"
surrealkv = { version = "0.3.2", optional = true } surrealkv = { version = "0.3.2", optional = true }
surrealml = { version = "0.1.1", optional = true, package = "surrealml-core" } surrealml = { version = "0.1.1", optional = true, package = "surrealml-core" }
tempfile = { version = "3.10.1", optional = true } tempfile = { version = "3.10.1", optional = true }

View file

@ -50,3 +50,12 @@ pub static INSECURE_FORWARD_ACCESS_ERRORS: Lazy<bool> =
/// If the environment variable is not present or cannot be parsed, a default value of 50,000 is used. /// 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<usize> = pub static EXTERNAL_SORTING_BUFFER_LIMIT: Lazy<usize> =
lazy_env_parse!("SURREAL_EXTERNAL_SORTING_BUFFER_LIMIT", usize, 50_000); 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<bool> =
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<bool> = Lazy::new(|| true);

View file

@ -9,6 +9,7 @@ use crate::sql::order::Orders;
use crate::sql::output::Output; use crate::sql::output::Output;
use crate::sql::split::Splits; use crate::sql::split::Splits;
use crate::sql::start::Start; use crate::sql::start::Start;
use crate::sql::statements::access::AccessStatement;
use crate::sql::statements::create::CreateStatement; use crate::sql::statements::create::CreateStatement;
use crate::sql::statements::delete::DeleteStatement; use crate::sql::statements::delete::DeleteStatement;
use crate::sql::statements::insert::InsertStatement; use crate::sql::statements::insert::InsertStatement;
@ -32,6 +33,9 @@ pub(crate) enum Statement<'a> {
Relate(&'a RelateStatement), Relate(&'a RelateStatement),
Delete(&'a DeleteStatement), Delete(&'a DeleteStatement),
Insert(&'a InsertStatement), 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> { 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> { impl<'a> fmt::Display for Statement<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
@ -100,6 +110,7 @@ impl<'a> fmt::Display for Statement<'a> {
Statement::Relate(v) => write!(f, "{v}"), Statement::Relate(v) => write!(f, "{v}"),
Statement::Delete(v) => write!(f, "{v}"), Statement::Delete(v) => write!(f, "{v}"),
Statement::Insert(v) => write!(f, "{v}"), Statement::Insert(v) => write!(f, "{v}"),
Statement::Access(v) => write!(f, "{v}"),
} }
} }
} }

View file

@ -932,43 +932,67 @@ pub enum Error {
Serialization(String), Serialization(String),
/// The requested root access method already exists /// The requested root access method already exists
#[error("The root access method '{value}' already exists")] #[error("The root access method '{ac}' already exists")]
AccessRootAlreadyExists { AccessRootAlreadyExists {
value: String, ac: String,
}, },
/// The requested namespace access method already exists /// 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 { AccessNsAlreadyExists {
value: String, ac: String,
ns: String, ns: String,
}, },
/// The requested database access method already exists /// 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 { AccessDbAlreadyExists {
value: String, ac: String,
ns: String, ns: String,
db: String, db: String,
}, },
/// The requested root access method does not exist /// 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 { 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 /// 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 { 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, ns: String,
}, },
/// The requested database access method does not exist /// 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 { 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, ns: String,
db: String, db: String,
}, },
@ -1001,6 +1025,18 @@ pub enum Error {
#[error("This record access method does not allow signin")] #[error("This record access method does not allow signin")]
AccessRecordNoSignin, 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 /// 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")] #[error("Found {value} for the Record ID but this is not a valid table name")]
TbInvalid { TbInvalid {

View file

@ -54,6 +54,7 @@ impl From<&Statement<'_>> for Action {
Statement::Relate(_) => Action::Edit, Statement::Relate(_) => Action::Edit,
Statement::Delete(_) => Action::Edit, Statement::Delete(_) => Action::Edit,
Statement::Insert(_) => Action::Edit, Statement::Insert(_) => Action::Edit,
Statement::Access(_) => Action::Edit,
} }
} }
} }

View file

@ -29,6 +29,7 @@ pub enum ResourceKind {
Event, Event,
Field, Field,
Index, Index,
Access,
// IAM // IAM
Actor, Actor,
@ -51,6 +52,7 @@ impl std::fmt::Display for ResourceKind {
ResourceKind::Event => write!(f, "Event"), ResourceKind::Event => write!(f, "Event"),
ResourceKind::Field => write!(f, "Field"), ResourceKind::Field => write!(f, "Field"),
ResourceKind::Index => write!(f, "Index"), ResourceKind::Index => write!(f, "Index"),
ResourceKind::Access => write!(f, "Access"),
ResourceKind::Actor => write!(f, "Actor"), ResourceKind::Actor => write!(f, "Actor"),
} }
} }

View file

@ -46,6 +46,7 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy<serde_json::Value> = Lazy::new(|| {
"Event": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Event": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Field": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Field": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Index": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Index": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Access": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
// IAM resource types // IAM resource types
"Role": {}, "Role": {},
@ -65,14 +66,14 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy<serde_json::Value> = Lazy::new(|| {
"View": { "View": {
"appliesTo": { "appliesTo": {
"principalTypes": [ "Actor" ], "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": { "Edit": {
"appliesTo": { "appliesTo": {
"principalTypes": [ "Actor" ], "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" ],
}, },
}, },
}, },

View file

@ -5,7 +5,7 @@ use chrono::Duration as ChronoDuration;
use chrono::Utc; use chrono::Utc;
use jsonwebtoken::EncodingKey; use jsonwebtoken::EncodingKey;
pub(crate) fn config(alg: Algorithm, key: String) -> Result<EncodingKey, Error> { pub(crate) fn config(alg: Algorithm, key: &str) -> Result<EncodingKey, Error> {
match alg { match alg {
Algorithm::Hs256 => Ok(EncodingKey::from_secret(key.as_ref())), Algorithm::Hs256 => Ok(EncodingKey::from_secret(key.as_ref())),
Algorithm::Hs384 => Ok(EncodingKey::from_secret(key.as_ref())), Algorithm::Hs384 => Ok(EncodingKey::from_secret(key.as_ref())),

File diff suppressed because it is too large Load diff

View file

@ -57,14 +57,14 @@ pub async fn db_access(
Ok(av) => { Ok(av) => {
// Check the access method type // Check the access method type
// Currently, only the record access method supports signup // Currently, only the record access method supports signup
match av.kind.clone() { match &av.kind {
AccessType::Record(at) => { AccessType::Record(at) => {
// Check if the record access method supports issuing tokens // Check if the record access method supports issuing tokens
let iss = match at.jwt.issue { let iss = match &at.jwt.issue {
Some(iss) => iss, Some(iss) => iss,
_ => return Err(Error::AccessMethodMismatch), _ => return Err(Error::AccessMethodMismatch),
}; };
match at.signup { match &at.signup {
// This record access allows signup // This record access allows signup
Some(val) => { Some(val) => {
// Setup the query params // Setup the query params
@ -81,7 +81,7 @@ pub async fn db_access(
// There is a record returned // There is a record returned
Some(mut rid) => { Some(mut rid) => {
// Create the authentication key // Create the authentication key
let key = config(iss.alg, iss.key)?; let key = config(iss.alg, &iss.key)?;
// Create the authentication claim // Create the authentication claim
let claims = Claims { let claims = Claims {
iss: Some(SERVER_NAME.to_owned()), iss: Some(SERVER_NAME.to_owned()),
@ -101,11 +101,11 @@ pub async fn db_access(
let mut sess = let mut sess =
Session::editor().with_ns(&ns).with_db(&db); Session::editor().with_ns(&ns).with_db(&db);
sess.rd = Some(rid.clone().into()); sess.rd = Some(rid.clone().into());
sess.tk = Some(claims.clone().into()); sess.tk = Some((&claims).into());
sess.ip.clone_from(&session.ip); sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or); sess.or.clone_from(&session.or);
// Compute the value with the params // 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() { Ok(val) => match val.record() {
Some(id) => { Some(id) => {
// Update rid with result from AUTHENTICATE clause // Update rid with result from AUTHENTICATE clause
@ -128,7 +128,7 @@ pub async fn db_access(
let enc = let enc =
encode(&Header::new(iss.alg.into()), &claims, &key); encode(&Header::new(iss.alg.into()), &claims, &key);
// Set the authentication on the session // Set the authentication on the session
session.tk = Some(claims.into()); session.tk = Some((&claims).into());
session.ns = Some(ns.to_owned()); session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned()); session.db = Some(db.to_owned());
session.ac = Some(ac.to_owned()); session.ac = Some(ac.to_owned());

View file

@ -147,7 +147,89 @@ impl From<Claims> for Value {
continue; 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 // Return value

View file

@ -5,7 +5,7 @@ use crate::err::Error;
use crate::iam::jwks; use crate::iam::jwks;
use crate::iam::{issue::expiration, token::Claims, Actor, Auth, Level, Role}; use crate::iam::{issue::expiration, token::Claims, Actor, Auth, Level, Role};
use crate::kvs::{Datastore, LockType::*, TransactionType::*}; 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::sql::{statements::DefineUserStatement, Algorithm, Thing, Value};
use crate::syn; use crate::syn;
use argon2::{Argon2, PasswordHash, PasswordVerifier}; 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 // Decode the token without verifying
let token_data = decode::<Claims>(token, &KEY, &DUD)?; let token_data = decode::<Claims>(token, &KEY, &DUD)?;
// Convert the token to a SurrealQL object value // 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 // Check if the auth token can be used
if let Some(nbf) = token_data.claims.nbf { if let Some(nbf) = token_data.claims.nbf {
if nbf > Utc::now().timestamp() { 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 // Check the token authentication claims
match token_data.claims.clone() { match &token_data.claims {
// Check if this is record access // Check if this is record access
Claims { Claims {
ns: Some(ns), ns: Some(ns),
@ -160,9 +160,9 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// Create a new readonly transaction // Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?; let tx = kvs.transaction(Read, Optimistic).await?;
// Parse the record id // Parse the record id
let mut rid = syn::thing(&id)?; let mut rid = syn::thing(id)?;
// Get the database access method // 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 // Ensure that the transaction is cancelled
tx.cancel().await?; tx.cancel().await?;
// Obtain the configuration to verify the token based on the access method // 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 // AUTHENTICATE clause
if let Some(au) = &de.authenticate { if let Some(au) = &de.authenticate {
// Setup the system session for finding the signin record // 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.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.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or); sess.or.clone_from(&session.or);
rid = authenticate_record(kvs, &sess, au.clone()).await?; rid = authenticate_record(kvs, &sess, au).await?;
} }
// Log the success // Log the success
debug!("Authenticated with record access method `{}`", ac); 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( session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(), rid.to_string(),
Default::default(), Default::default(),
Level::Record(ns, db, rid.to_string()), Level::Record(ns.to_string(), db.to_string(), rid.to_string()),
))); )));
Ok(()) Ok(())
} }
@ -223,14 +223,14 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// Create a new readonly transaction // Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?; let tx = kvs.transaction(Read, Optimistic).await?;
// Get the database access method // 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 // Ensure that the transaction is cancelled
tx.cancel().await?; tx.cancel().await?;
// Obtain the configuration to verify the token based on the access method // Obtain the configuration to verify the token based on the access method
match &de.kind { match &de.kind {
// If the access type is Jwt, this is database access // If the access type is Jwt or Bearer, this is database access
AccessType::Jwt(at) => { AccessType::Jwt(_) | AccessType::Bearer(_) => {
let cf = match &at.verify { let cf = match &de.kind.jwt().verify {
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()), JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
#[cfg(feature = "jwks")] #[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => { JwtAccessVerify::Jwks(jwks) => {
@ -247,15 +247,15 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
decode::<Claims>(token, &cf.0, &cf.1)?; decode::<Claims>(token, &cf.0, &cf.1)?;
// AUTHENTICATE clause // AUTHENTICATE clause
if let Some(au) = &de.authenticate { 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().with_ns(&ns).with_db(&db); let mut sess = Session::editor().with_ns(ns).with_db(db);
sess.tk = Some(token_data.claims.clone().into()); sess.tk = Some((&token_data.claims).into());
sess.ip.clone_from(&session.ip); sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or); sess.or.clone_from(&session.or);
authenticate_jwt(kvs, &sess, au.clone()).await?; authenticate_generic(kvs, &sess, au).await?;
} }
// Parse the roles // 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 // If no role is provided, grant the viewer role
None => vec![Role::Viewer], None => vec![Role::Viewer],
// If roles are provided, parse them // 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( session.au = Arc::new(Auth::new(Actor::new(
de.name.to_string(), de.name.to_string(),
roles, roles,
Level::Database(ns, db), Level::Database(ns.to_string(), db.to_string()),
))); )));
} }
// If the access type is Record, this is record access // 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::<Claims>(token, &cf.0, &cf.1)?; decode::<Claims>(token, &cf.0, &cf.1)?;
// AUTHENTICATE clause // AUTHENTICATE clause
// Setup the system session for finding the signin record // 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.tk = Some(token_data.claims.clone().into()); sess.tk = Some((&token_data.claims).into());
sess.ip.clone_from(&session.ip); sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or); 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 // Log the success
debug!("Authenticated with record access method `{}`", ac); debug!("Authenticated with record access method `{}`", ac);
// Set the session // 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( session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(), rid.to_string(),
Default::default(), Default::default(),
Level::Record(ns, db, rid.to_string()), Level::Record(ns.to_string(), db.to_string(), rid.to_string()),
))); )));
} }
_ => return Err(Error::AccessMethodMismatch), _ => 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 // Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?; let tx = kvs.transaction(Read, Optimistic).await?;
// Get the database user // 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}"); trace!("Error while authenticating to database `{db}`: {e}");
Error::InvalidAuth 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( session.au = Arc::new(Auth::new(Actor::new(
id.to_string(), id.to_string(),
de.roles.iter().map(|r| r.into()).collect(), de.roles.iter().map(|r| r.into()).collect(),
Level::Database(ns, db), Level::Database(ns.to_string(), db.to_string()),
))); )));
Ok(()) Ok(())
} }
@ -376,12 +376,12 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// Create a new readonly transaction // Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?; let tx = kvs.transaction(Read, Optimistic).await?;
// Get the namespace access method // 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 // Ensure that the transaction is cancelled
tx.cancel().await?; tx.cancel().await?;
// Obtain the configuration to verify the token based on the access method // Obtain the configuration to verify the token based on the access method
let cf = match &de.kind { 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()), JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
#[cfg(feature = "jwks")] #[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => { JwtAccessVerify::Jwks(jwks) => {
@ -400,15 +400,15 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
decode::<Claims>(token, &cf.0, &cf.1)?; decode::<Claims>(token, &cf.0, &cf.1)?;
// AUTHENTICATE clause // AUTHENTICATE clause
if let Some(au) = &de.authenticate { 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().with_ns(&ns); let mut sess = Session::editor().with_ns(ns);
sess.tk = Some(token_data.claims.clone().into()); sess.tk = Some((&token_data.claims).into());
sess.ip.clone_from(&session.ip); sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or); sess.or.clone_from(&session.or);
authenticate_jwt(kvs, &sess, au.clone()).await?; authenticate_generic(kvs, &sess, au).await?;
} }
// Parse the roles // 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 // If no role is provided, grant the viewer role
None => vec![Role::Viewer], None => vec![Role::Viewer],
// If roles are provided, parse them // 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.ns = Some(ns.to_owned());
session.ac = Some(ac.to_owned()); session.ac = Some(ac.to_owned());
session.exp = expiration(de.duration.session)?; session.exp = expiration(de.duration.session)?;
session.au = session.au = Arc::new(Auth::new(Actor::new(
Arc::new(Auth::new(Actor::new(de.name.to_string(), roles, Level::Namespace(ns)))); de.name.to_string(),
roles,
Level::Namespace(ns.to_string()),
)));
Ok(()) Ok(())
} }
// Check if this is namespace authentication with user credentials // 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 // Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?; let tx = kvs.transaction(Read, Optimistic).await?;
// Get the namespace user // 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}"); trace!("Error while authenticating to namespace `{ns}`: {e}");
Error::InvalidAuth 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( session.au = Arc::new(Auth::new(Actor::new(
id.to_string(), id.to_string(),
de.roles.iter().map(|r| r.into()).collect(), de.roles.iter().map(|r| r.into()).collect(),
Level::Namespace(ns), Level::Namespace(ns.to_string()),
))); )));
Ok(()) Ok(())
} }
@ -474,12 +477,12 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
// Create a new readonly transaction // Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?; let tx = kvs.transaction(Read, Optimistic).await?;
// Get the namespace access method // 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 // Ensure that the transaction is cancelled
tx.cancel().await?; tx.cancel().await?;
// Obtain the configuration to verify the token based on the access method // Obtain the configuration to verify the token based on the access method
let cf = match &de.kind { 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()), JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
#[cfg(feature = "jwks")] #[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => { JwtAccessVerify::Jwks(jwks) => {
@ -498,15 +501,15 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
decode::<Claims>(token, &cf.0, &cf.1)?; decode::<Claims>(token, &cf.0, &cf.1)?;
// AUTHENTICATE clause // AUTHENTICATE clause
if let Some(au) = &de.authenticate { 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(); 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.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or); sess.or.clone_from(&session.or);
authenticate_jwt(kvs, &sess, au.clone()).await?; authenticate_generic(kvs, &sess, au).await?;
} }
// Parse the roles // 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 // If no role is provided, grant the viewer role
None => vec![Role::Viewer], None => vec![Role::Viewer],
// If roles are provided, parse them // 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 // Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?; let tx = kvs.transaction(Read, Optimistic).await?;
// Get the namespace user // 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}"); trace!("Error while authenticating to root: {e}");
Error::InvalidAuth Error::InvalidAuth
})?; })?;
@ -642,11 +645,11 @@ fn verify_pass(pass: &str, hash: &str) -> Result<(), Error> {
} }
} }
// Execute the AUTHENTICATE clause for a record access method // Execute the AUTHENTICATE clause for a Record access method
async fn authenticate_record( pub async fn authenticate_record(
kvs: &Datastore, kvs: &Datastore,
session: &Session, session: &Session,
authenticate: Value, authenticate: &Value,
) -> Result<Thing, Error> { ) -> Result<Thing, Error> {
match kvs.evaluate(authenticate, session, None).await { match kvs.evaluate(authenticate, session, None).await {
Ok(val) => match val.record() { Ok(val) => match val.record() {
@ -664,11 +667,11 @@ async fn authenticate_record(
} }
} }
// Execute the AUTHENTICATE clause for a JWT access method // Execute the AUTHENTICATE clause for any other access method
async fn authenticate_jwt( pub async fn authenticate_generic(
kvs: &Datastore, kvs: &Datastore,
session: &Session, session: &Session,
authenticate: Value, authenticate: &Value,
) -> Result<(), Error> { ) -> Result<(), Error> {
match kvs.evaluate(authenticate, session, None).await { match kvs.evaluate(authenticate, session, None).await {
Ok(val) => { Ok(val) => {

View file

@ -12,8 +12,12 @@ pub(crate) trait Categorise {
pub enum Category { pub enum Category {
/// crate::key::root::all / /// crate::key::root::all /
Root, Root,
/// crate::key::root::ac /!ac{ac} /// crate::key::root::access::ac /!ac{ac}
Access, Access,
/// crate::key::root::access::all /*{ac}
AccessRoot,
/// crate::key::root::access::gr /*{ac}!gr{gr}
AccessGrant,
/// crate::key::root::nd /!nd{nd} /// crate::key::root::nd /!nd{nd}
Node, Node,
/// crate::key::root::ni /!ni /// crate::key::root::ni /!ni
@ -43,8 +47,12 @@ pub enum Category {
NamespaceRoot, NamespaceRoot,
/// crate::key::namespace::db /*{ns}!db{db} /// crate::key::namespace::db /*{ns}!db{db}
DatabaseAlias, DatabaseAlias,
/// crate::key::namespace::ac /*{ns}!ac{ac} /// crate::key::namespace::access::ac /*{ns}!ac{ac}
NamespaceAccess, 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} /// crate::key::namespace::us /*{ns}!us{us}
NamespaceUser, NamespaceUser,
/// ///
@ -52,8 +60,12 @@ pub enum Category {
/// ///
/// crate::key::database::all /*{ns}*{db} /// crate::key::database::all /*{ns}*{db}
DatabaseRoot, DatabaseRoot,
/// crate::key::database::ac /*{ns}*{db}!ac{ac} /// crate::key::database::access::ac /*{ns}*{db}!ac{ac}
DatabaseAccess, 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} /// crate::key::database::az /*{ns}*{db}!az{az}
DatabaseAnalyzer, DatabaseAnalyzer,
/// crate::key::database::fc /*{ns}*{db}!fn{fc} /// crate::key::database::fc /*{ns}*{db}!fn{fc}
@ -136,6 +148,8 @@ impl Display for Category {
let name = match self { let name = match self {
Self::Root => "Root", Self::Root => "Root",
Self::Access => "Access", Self::Access => "Access",
Self::AccessRoot => "AccessRoot",
Self::AccessGrant => "AccessGrant",
Self::Node => "Node", Self::Node => "Node",
Self::NamespaceIdentifier => "NamespaceIdentifier", Self::NamespaceIdentifier => "NamespaceIdentifier",
Self::Namespace => "Namespace", Self::Namespace => "Namespace",
@ -146,9 +160,13 @@ impl Display for Category {
Self::DatabaseAlias => "DatabaseAlias", Self::DatabaseAlias => "DatabaseAlias",
Self::DatabaseIdentifier => "DatabaseIdentifier", Self::DatabaseIdentifier => "DatabaseIdentifier",
Self::NamespaceAccess => "NamespaceAccess", Self::NamespaceAccess => "NamespaceAccess",
Self::NamespaceAccessRoot => "NamespaceAccessRoot",
Self::NamespaceAccessGrant => "NamespaceAccessGrant",
Self::NamespaceUser => "NamespaceUser", Self::NamespaceUser => "NamespaceUser",
Self::DatabaseRoot => "DatabaseRoot", Self::DatabaseRoot => "DatabaseRoot",
Self::DatabaseAccess => "DatabaseAccess", Self::DatabaseAccess => "DatabaseAccess",
Self::DatabaseAccessRoot => "DatabaseAccessRoot",
Self::DatabaseAccessGrant => "DatabaseAccessGrant",
Self::DatabaseAnalyzer => "DatabaseAnalyzer", Self::DatabaseAnalyzer => "DatabaseAnalyzer",
Self::DatabaseFunction => "DatabaseFunction", Self::DatabaseFunction => "DatabaseFunction",
Self::DatabaseModel => "DatabaseModel", Self::DatabaseModel => "DatabaseModel",

View file

@ -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::Categorise;
use crate::key::category::Category; use crate::key::category::Category;
use derive::Key; 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<u8> { pub fn prefix(ns: &str, db: &str) -> Vec<u8> {
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.extend_from_slice(&[b'!', b'a', b'c', 0x00]);
k k
} }
pub fn suffix(ns: &str, db: &str) -> Vec<u8> { pub fn suffix(ns: &str, db: &str) -> Vec<u8> {
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.extend_from_slice(&[b'!', b'a', b'c', 0xff]);
k k
} }
@ -68,7 +68,7 @@ mod tests {
"testac", "testac",
); );
let enc = Ac::encode(&val).unwrap(); 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(); let dec = Ac::decode(&enc).unwrap();
assert_eq!(val, dec); assert_eq!(val, dec);

View file

@ -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);
}
}

View file

@ -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<u8> {
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<u8> {
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");
}
}

View file

@ -0,0 +1,3 @@
pub mod ac;
pub mod all;
pub mod gr;

View file

@ -1,4 +1,4 @@
pub mod ac; pub mod access;
pub mod all; pub mod all;
pub mod az; pub mod az;
pub mod fc; pub mod fc;

View file

@ -1,7 +1,9 @@
//! How the keys are structured in the key value store //! How the keys are structured in the key value store
/// ///
/// crate::key::root::all / /// crate::key::root::all /
/// crate::key::root::ac /!ac{ac} /// crate::key::root::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::hb /!hb{ts}/{nd}
/// crate::key::root::nd /!nd{nd} /// crate::key::root::nd /!nd{nd}
/// crate::key::root::ni /!ni /// crate::key::root::ni /!ni
@ -12,14 +14,18 @@
/// crate::key::node::lq /${nd}!lq{lq}{ns}{db} /// crate::key::node::lq /${nd}!lq{lq}{ns}{db}
/// ///
/// crate::key::namespace::all /*{ns} /// 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::db /*{ns}!db{db}
/// crate::key::namespace::di /+{ns id}!di /// crate::key::namespace::di /+{ns id}!di
/// crate::key::namespace::lg /*{ns}!lg{lg} /// crate::key::namespace::lg /*{ns}!lg{lg}
/// crate::key::namespace::us /*{ns}!us{us} /// crate::key::namespace::us /*{ns}!us{us}
/// ///
/// crate::key::database::all /*{ns}*{db} /// 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::az /*{ns}*{db}!az{az}
/// crate::key::database::fc /*{ns}*{db}!fn{fc} /// crate::key::database::fc /*{ns}*{db}!fn{fc}
/// crate::key::database::ml /*{ns}*{db}!ml{ml}{vn} /// crate::key::database::ml /*{ns}*{db}!ml{ml}{vn}

View file

@ -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::Categorise;
use crate::key::category::Category; use crate::key::category::Category;
use derive::Key; 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<u8> { pub fn prefix(ns: &str) -> Vec<u8> {
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.extend_from_slice(&[b'!', b'a', b'c', 0x00]);
k k
} }
pub fn suffix(ns: &str) -> Vec<u8> { pub fn suffix(ns: &str) -> Vec<u8> {
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.extend_from_slice(&[b'!', b'a', b'c', 0xff]);
k k
} }
@ -64,7 +64,20 @@ mod tests {
); );
let enc = Ac::encode(&val).unwrap(); let enc = Ac::encode(&val).unwrap();
assert_eq!(enc, b"/*testns\0!actestac\0"); assert_eq!(enc, b"/*testns\0!actestac\0");
let dec = Ac::decode(&enc).unwrap(); let dec = Ac::decode(&enc).unwrap();
assert_eq!(val, dec); 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");
}
} }

View file

@ -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);
}
}

View file

@ -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<u8> {
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<u8> {
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");
}
}

View file

@ -0,0 +1,3 @@
pub mod ac;
pub mod all;
pub mod gr;

View file

@ -1,4 +1,4 @@
pub mod ac; pub mod access;
pub mod all; pub mod all;
pub mod db; pub mod db;
pub mod di; pub mod di;

View file

@ -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::Categorise;
use crate::key::category::Category; use crate::key::category::Category;
use derive::Key; use derive::Key;
@ -19,13 +19,13 @@ pub fn new(ac: &str) -> Ac<'_> {
} }
pub fn prefix() -> Vec<u8> { pub fn prefix() -> Vec<u8> {
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.extend_from_slice(&[b'!', b'a', b'c', 0x00]);
k k
} }
pub fn suffix() -> Vec<u8> { pub fn suffix() -> Vec<u8> {
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.extend_from_slice(&[b'!', b'a', b'c', 0xff]);
k k
} }

View file

@ -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);
}
}

View file

@ -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<u8> {
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<u8> {
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");
}
}

View file

@ -0,0 +1,3 @@
pub mod ac;
pub mod all;
pub mod gr;

View file

@ -1,4 +1,4 @@
pub mod ac; pub mod access;
pub mod all; pub mod all;
pub mod nd; pub mod nd;
pub mod ni; pub mod ni;

View file

@ -1,5 +1,6 @@
use super::Key; use super::Key;
use crate::dbs::node::Node; use crate::dbs::node::Node;
use crate::sql::statements::AccessGrant;
use crate::sql::statements::DefineAccessStatement; use crate::sql::statements::DefineAccessStatement;
use crate::sql::statements::DefineAnalyzerStatement; use crate::sql::statements::DefineAnalyzerStatement;
use crate::sql::statements::DefineDatabaseStatement; use crate::sql::statements::DefineDatabaseStatement;
@ -53,18 +54,24 @@ pub(super) enum Entry {
Rus(Arc<[DefineUserStatement]>), Rus(Arc<[DefineUserStatement]>),
/// A slice of DefineAccessStatement specified at the root. /// A slice of DefineAccessStatement specified at the root.
Ras(Arc<[DefineAccessStatement]>), Ras(Arc<[DefineAccessStatement]>),
/// A slice of AccessGrant specified at the root.
Rag(Arc<[AccessGrant]>),
/// A slice of DefineNamespaceStatement specified on a namespace. /// A slice of DefineNamespaceStatement specified on a namespace.
Nss(Arc<[DefineNamespaceStatement]>), Nss(Arc<[DefineNamespaceStatement]>),
/// A slice of DefineUserStatement specified on a namespace. /// A slice of DefineUserStatement specified on a namespace.
Nus(Arc<[DefineUserStatement]>), Nus(Arc<[DefineUserStatement]>),
/// A slice of DefineAccessStatement specified on a namespace. /// A slice of DefineAccessStatement specified on a namespace.
Nas(Arc<[DefineAccessStatement]>), Nas(Arc<[DefineAccessStatement]>),
/// A slice of AccessGrant specified at on a namespace.
Nag(Arc<[AccessGrant]>),
/// A slice of DefineDatabaseStatement specified on a namespace. /// A slice of DefineDatabaseStatement specified on a namespace.
Dbs(Arc<[DefineDatabaseStatement]>), Dbs(Arc<[DefineDatabaseStatement]>),
/// A slice of DefineAnalyzerStatement specified on a namespace. /// A slice of DefineAnalyzerStatement specified on a namespace.
Azs(Arc<[DefineAnalyzerStatement]>), Azs(Arc<[DefineAnalyzerStatement]>),
/// A slice of DefineAccessStatement specified on a database. /// A slice of DefineAccessStatement specified on a database.
Das(Arc<[DefineAccessStatement]>), Das(Arc<[DefineAccessStatement]>),
/// A slice of AccessGrant specified at on a database.
Dag(Arc<[AccessGrant]>),
/// A slice of DefineUserStatement specified on a database. /// A slice of DefineUserStatement specified on a database.
Dus(Arc<[DefineUserStatement]>), Dus(Arc<[DefineUserStatement]>),
/// A slice of DefineFunctionStatement specified on a database. /// A slice of DefineFunctionStatement specified on a database.
@ -120,6 +127,14 @@ impl Entry {
_ => unreachable!(), _ => 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`]. /// Converts this cache entry into a slice of [`DefineNamespaceStatement`].
/// This panics if called on a cache entry that is not an [`Entry::Nss`]. /// This panics if called on a cache entry that is not an [`Entry::Nss`].
pub(super) fn into_nss(self) -> Arc<[DefineNamespaceStatement]> { pub(super) fn into_nss(self) -> Arc<[DefineNamespaceStatement]> {
@ -136,6 +151,14 @@ impl Entry {
_ => unreachable!(), _ => 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`]. /// Converts this cache entry into a slice of [`DefineUserStatement`].
/// This panics if called on a cache entry that is not an [`Entry::Nus`]. /// This panics if called on a cache entry that is not an [`Entry::Nus`].
pub(super) fn into_nus(self) -> Arc<[DefineUserStatement]> { pub(super) fn into_nus(self) -> Arc<[DefineUserStatement]> {
@ -160,6 +183,14 @@ impl Entry {
_ => unreachable!(), _ => 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`]. /// Converts this cache entry into a slice of [`DefineUserStatement`].
/// This panics if called on a cache entry that is not an [`Entry::Dus`]. /// This panics if called on a cache entry that is not an [`Entry::Dus`].
pub(super) fn into_dus(self) -> Arc<[DefineUserStatement]> { pub(super) fn into_dus(self) -> Arc<[DefineUserStatement]> {

View file

@ -810,14 +810,14 @@ impl Datastore {
/// let ds = Datastore::new("memory").await?; /// let ds = Datastore::new("memory").await?;
/// let ses = Session::owner(); /// let ses = Session::owner();
/// let val = Value::Future(Box::new(Future::from(Value::Bool(true)))); /// 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(()) /// Ok(())
/// } /// }
/// ``` /// ```
#[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)] #[instrument(level = "debug", target = "surrealdb::core::kvs::ds", skip_all)]
pub async fn evaluate( pub async fn evaluate(
&self, &self,
val: Value, val: &Value,
sess: &Session, sess: &Session,
vars: Variables, vars: Variables,
) -> Result<Value, Error> { ) -> Result<Value, Error> {

View file

@ -11,6 +11,7 @@ use crate::kvs::cache::Entry;
use crate::kvs::cache::EntryWeighter; use crate::kvs::cache::EntryWeighter;
use crate::kvs::scanner::Scanner; use crate::kvs::scanner::Scanner;
use crate::kvs::Transactor; use crate::kvs::Transactor;
use crate::sql::statements::AccessGrant;
use crate::sql::statements::DefineAccessStatement; use crate::sql::statements::DefineAccessStatement;
use crate::sql::statements::DefineAnalyzerStatement; use crate::sql::statements::DefineAnalyzerStatement;
use crate::sql::statements::DefineDatabaseStatement; use crate::sql::statements::DefineDatabaseStatement;
@ -344,13 +345,14 @@ impl Transaction {
} }
/// Retrieve all ROOT level accesses in a datastore. /// 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<Arc<[DefineAccessStatement]>, Error> { pub async fn all_root_accesses(&self) -> Result<Arc<[DefineAccessStatement]>, 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; let res = self.cache.get_value_or_guard_async(&key).await;
Ok(match res { Ok(match res {
Ok(val) => val, Ok(val) => val,
Err(cache) => { 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 = self.getr(key..end).await?;
let val = val.convert().into(); let val = val.convert().into();
let val = Entry::Ras(Arc::clone(&val)); let val = Entry::Ras(Arc::clone(&val));
@ -361,6 +363,25 @@ impl Transaction {
.into_ras()) .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<Arc<[AccessGrant]>, 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. /// Retrieve all namespace definitions in a datastore.
#[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))]
pub async fn all_ns(&self) -> Result<Arc<[DefineNamespaceStatement]>, Error> { pub async fn all_ns(&self) -> Result<Arc<[DefineNamespaceStatement]>, Error> {
@ -402,12 +423,12 @@ impl Transaction {
/// Retrieve all namespace access definitions for a specific namespace. /// Retrieve all namespace access definitions for a specific namespace.
#[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))]
pub async fn all_ns_accesses(&self, ns: &str) -> Result<Arc<[DefineAccessStatement]>, Error> { pub async fn all_ns_accesses(&self, ns: &str) -> Result<Arc<[DefineAccessStatement]>, 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; let res = self.cache.get_value_or_guard_async(&key).await;
Ok(match res { Ok(match res {
Ok(val) => val, Ok(val) => val,
Err(cache) => { 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 = self.getr(key..end).await?;
let val = val.convert().into(); let val = val.convert().into();
let val = Entry::Nas(Arc::clone(&val)); let val = Entry::Nas(Arc::clone(&val));
@ -418,6 +439,29 @@ impl Transaction {
.into_nas()) .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<Arc<[AccessGrant]>, 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. /// Retrieve all database definitions for a specific namespace.
#[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))]
pub async fn all_db(&self, ns: &str) -> Result<Arc<[DefineDatabaseStatement]>, Error> { pub async fn all_db(&self, ns: &str) -> Result<Arc<[DefineDatabaseStatement]>, Error> {
@ -467,12 +511,12 @@ impl Transaction {
ns: &str, ns: &str,
db: &str, db: &str,
) -> Result<Arc<[DefineAccessStatement]>, Error> { ) -> Result<Arc<[DefineAccessStatement]>, 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; let res = self.cache.get_value_or_guard_async(&key).await;
Ok(match res { Ok(match res {
Ok(val) => val, Ok(val) => val,
Err(cache) => { 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 = self.getr(key..end).await?;
let val = val.convert().into(); let val = val.convert().into();
let val = Entry::Das(Arc::clone(&val)); let val = Entry::Das(Arc::clone(&val));
@ -483,6 +527,30 @@ impl Transaction {
.into_das()) .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<Arc<[AccessGrant]>, 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. /// Retrieve all analyzer definitions for a specific database.
#[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))]
pub async fn all_db_analyzers( pub async fn all_db_analyzers(
@ -734,7 +802,7 @@ impl Transaction {
.into_type()) .into_type())
} }
/// Retrieve a specific namespace user definition. /// Retrieve a specific root user definition.
#[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))]
pub async fn get_root_user(&self, us: &str) -> Result<Arc<DefineUserStatement>, Error> { pub async fn get_root_user(&self, us: &str) -> Result<Arc<DefineUserStatement>, Error> {
let key = crate::key::root::us::new(us).encode()?; let key = crate::key::root::us::new(us).encode()?;
@ -754,15 +822,16 @@ impl Transaction {
.into_type()) .into_type())
} }
/// Retrieve a specific namespace user definition. /// Retrieve a specific root access definition.
pub async fn get_root_access(&self, user: &str) -> Result<Arc<DefineAccessStatement>, Error> { #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))]
let key = crate::key::root::ac::new(user).encode()?; pub async fn get_root_access(&self, ra: &str) -> Result<Arc<DefineAccessStatement>, Error> {
let key = crate::key::root::access::ac::new(ra).encode()?;
let res = self.cache.get_value_or_guard_async(&key).await; let res = self.cache.get_value_or_guard_async(&key).await;
Ok(match res { Ok(match res {
Ok(val) => val, Ok(val) => val,
Err(cache) => { Err(cache) => {
let val = self.get(key).await?.ok_or(Error::AccessRootNotFound { 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: DefineAccessStatement = val.into();
let val = Entry::Any(Arc::new(val)); let val = Entry::Any(Arc::new(val));
@ -773,6 +842,31 @@ impl Transaction {
.into_type()) .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<Arc<AccessGrant>, 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. /// Retrieve a specific namespace definition.
#[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))]
pub async fn get_ns(&self, ns: &str) -> Result<Arc<DefineNamespaceStatement>, Error> { pub async fn get_ns(&self, ns: &str) -> Result<Arc<DefineNamespaceStatement>, Error> {
@ -821,13 +915,13 @@ impl Transaction {
ns: &str, ns: &str,
na: &str, na: &str,
) -> Result<Arc<DefineAccessStatement>, Error> { ) -> Result<Arc<DefineAccessStatement>, 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; let res = self.cache.get_value_or_guard_async(&key).await;
Ok(match res { Ok(match res {
Ok(val) => val, Ok(val) => val,
Err(cache) => { Err(cache) => {
let val = self.get(key).await?.ok_or(Error::AccessNsNotFound { let val = self.get(key).await?.ok_or(Error::AccessNsNotFound {
value: na.to_owned(), ac: na.to_owned(),
ns: ns.to_owned(), ns: ns.to_owned(),
})?; })?;
let val: DefineAccessStatement = val.into(); let val: DefineAccessStatement = val.into();
@ -839,6 +933,33 @@ impl Transaction {
.into_type()) .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<Arc<AccessGrant>, 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. /// Retrieve a specific database definition.
#[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))]
pub async fn get_db(&self, ns: &str, db: &str) -> Result<Arc<DefineDatabaseStatement>, Error> { pub async fn get_db(&self, ns: &str, db: &str) -> Result<Arc<DefineDatabaseStatement>, Error> {
@ -894,13 +1015,13 @@ impl Transaction {
db: &str, db: &str,
da: &str, da: &str,
) -> Result<Arc<DefineAccessStatement>, Error> { ) -> Result<Arc<DefineAccessStatement>, 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; let res = self.cache.get_value_or_guard_async(&key).await;
Ok(match res { Ok(match res {
Ok(val) => val, Ok(val) => val,
Err(cache) => { Err(cache) => {
let val = self.get(key).await?.ok_or(Error::AccessDbNotFound { let val = self.get(key).await?.ok_or(Error::AccessDbNotFound {
value: da.to_owned(), ac: da.to_owned(),
ns: ns.to_owned(), ns: ns.to_owned(),
db: db.to_owned(), db: db.to_owned(),
})?; })?;
@ -913,6 +1034,35 @@ impl Transaction {
.into_type()) .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<Arc<AccessGrant>, 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. /// Retrieve a specific model definition from a database.
#[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))] #[instrument(level = "trace", target = "surrealdb::core::kvs::tx", skip(self))]
pub async fn get_db_model( pub async fn get_db_model(

View file

@ -9,13 +9,22 @@ use std::fmt;
use std::fmt::Display; use std::fmt::Display;
/// The type of access methods available /// The type of access methods available
#[revisioned(revision = 1)] #[revisioned(revision = 2)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)] #[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive] #[non_exhaustive]
pub enum AccessType { pub enum AccessType {
Record(RecordAccess), Record(RecordAccess),
Jwt(JwtAccess), 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 { 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 { impl Display for AccessType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
@ -43,6 +62,12 @@ impl Display for AccessType {
} }
write!(f, " WITH JWT {}", ac.jwt)?; write!(f, " WITH JWT {}", ac.jwt)?;
} }
AccessType::Bearer(ac) => {
write!(f, "BEARER")?;
if let BearerAccessLevel::Record = ac.level {
write!(f, " FOR RECORD")?;
}
}
} }
Ok(()) Ok(())
} }
@ -61,11 +86,21 @@ impl InfoStructure for AccessType {
"signup".to_string(), if let Some(v) = v.signup => v.structure(), "signup".to_string(), if let Some(v) = v.signup => v.structure(),
"signin".to_string(), if let Some(v) = v.signin => 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 { 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 /// Returns whether or not the access method can issue non-token grants
/// In this context, token refers exclusively to JWT /// In this context, token refers exclusively to JWT
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
@ -73,8 +108,7 @@ impl AccessType {
match self { match self {
// The grants for JWT and record access methods are JWT // The grants for JWT and record access methods are JWT
AccessType::Jwt(_) | AccessType::Record(_) => false, AccessType::Jwt(_) | AccessType::Record(_) => false,
// TODO(gguillemas): This arm should be reachable by the bearer access method AccessType::Bearer(_) => true,
_ => unreachable!(),
} }
} }
/// Returns whether or not the access method can issue tokens /// 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 { impl Display for JwtAccess {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.verify { 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,
}

View file

@ -3,6 +3,7 @@ use crate::dbs::Options;
use crate::doc::CursorDoc; use crate::doc::CursorDoc;
use crate::err::Error; use crate::err::Error;
use crate::sql::statements::rebuild::RebuildStatement; use crate::sql::statements::rebuild::RebuildStatement;
use crate::sql::statements::AccessStatement;
use crate::sql::{ use crate::sql::{
fmt::{Fmt, Pretty}, fmt::{Fmt, Pretty},
statements::{ 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)] #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive] #[non_exhaustive]
@ -93,6 +94,10 @@ pub enum Statement {
Upsert(UpsertStatement), Upsert(UpsertStatement),
#[revision(start = 4)] #[revision(start = 4)]
Alter(AlterStatement), Alter(AlterStatement),
// TODO(gguillemas): Document once bearer access is no longer experimental.
#[doc(hidden)]
#[revision(start = 5)]
Access(AccessStatement),
} }
impl Statement { impl Statement {
@ -113,6 +118,7 @@ impl Statement {
pub(crate) fn writeable(&self) -> bool { pub(crate) fn writeable(&self) -> bool {
match self { match self {
Self::Value(v) => v.writeable(), Self::Value(v) => v.writeable(),
Self::Access(_) => true,
Self::Alter(_) => true, Self::Alter(_) => true,
Self::Analyze(_) => false, Self::Analyze(_) => false,
Self::Break(_) => false, Self::Break(_) => false,
@ -151,6 +157,7 @@ impl Statement {
doc: Option<&CursorDoc<'_>>, doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> { ) -> Result<Value, Error> {
match self { match self {
Self::Access(v) => v.compute(ctx, opt, doc).await,
Self::Alter(v) => v.compute(stk, ctx, opt, doc).await, Self::Alter(v) => v.compute(stk, ctx, opt, doc).await,
Self::Analyze(v) => v.compute(ctx, opt, doc).await, Self::Analyze(v) => v.compute(ctx, opt, doc).await,
Self::Break(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 { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self { match self {
Self::Value(v) => write!(Pretty::from(f), "{v}"), 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::Alter(v) => write!(Pretty::from(f), "{v}"),
Self::Analyze(v) => write!(Pretty::from(f), "{v}"), Self::Analyze(v) => write!(Pretty::from(f), "{v}"),
Self::Begin(v) => write!(Pretty::from(f), "{v}"), Self::Begin(v) => write!(Pretty::from(f), "{v}"),

View file

@ -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<Base>,
}
// 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<Base>,
pub subject: Option<Subject>,
}
// 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<Base>,
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<Datetime>, // Grant expiration time, if any.
pub revocation: Option<Datetime>, // Grant revocation time, if any.
pub subject: Option<Subject>, // 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<AccessGrant> 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<Strand>, // 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<Strand>, // 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<Value, Error> {
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<Value, Error> {
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<Value, Error> {
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<Value, Error> {
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),
}
}
}

View file

@ -48,6 +48,10 @@ impl DefineAccessStatement {
ac.jwt = ac.jwt.redacted(); ac.jwt = ac.jwt.redacted();
AccessType::Record(ac) AccessType::Record(ac)
} }
AccessType::Bearer(mut ac) => {
ac.jwt = ac.jwt.redacted();
AccessType::Bearer(ac)
}
}; };
das das
} }
@ -74,12 +78,12 @@ impl DefineAccessStatement {
return Ok(Value::None); return Ok(Value::None);
} else if !self.overwrite { } else if !self.overwrite {
return Err(Error::AccessRootAlreadyExists { return Err(Error::AccessRootAlreadyExists {
value: self.name.to_string(), ac: self.name.to_string(),
}); });
} }
} }
// Process the statement // Process the statement
let key = crate::key::root::ac::new(&self.name); let key = crate::key::root::access::ac::new(&self.name);
txn.set( txn.set(
key, key,
DefineAccessStatement { DefineAccessStatement {
@ -104,13 +108,13 @@ impl DefineAccessStatement {
return Ok(Value::None); return Ok(Value::None);
} else if !self.overwrite { } else if !self.overwrite {
return Err(Error::AccessNsAlreadyExists { return Err(Error::AccessNsAlreadyExists {
value: self.name.to_string(), ac: self.name.to_string(),
ns: opt.ns()?.into(), ns: opt.ns()?.into(),
}); });
} }
} }
// Process the statement // 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.get_or_add_ns(opt.ns()?, opt.strict).await?;
txn.set( txn.set(
key, key,
@ -136,14 +140,14 @@ impl DefineAccessStatement {
return Ok(Value::None); return Ok(Value::None);
} else if !self.overwrite { } else if !self.overwrite {
return Err(Error::AccessDbAlreadyExists { return Err(Error::AccessDbAlreadyExists {
value: self.name.to_string(), ac: self.name.to_string(),
ns: opt.ns()?.into(), ns: opt.ns()?.into(),
db: opt.db()?.into(), db: opt.db()?.into(),
}); });
} }
} }
// Process the statement // 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_ns(opt.ns()?, opt.strict).await?;
txn.get_or_add_db(opt.ns()?, opt.db()?, opt.strict).await?; txn.get_or_add_db(opt.ns()?, opt.db()?, opt.strict).await?;
txn.set( txn.set(

View file

@ -1,3 +1,4 @@
pub(crate) mod access;
pub(crate) mod alter; pub(crate) mod alter;
pub(crate) mod analyze; pub(crate) mod analyze;
pub(crate) mod begin; pub(crate) mod begin;
@ -28,6 +29,9 @@ pub(crate) mod update;
pub(crate) mod upsert; pub(crate) mod upsert;
pub(crate) mod r#use; 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::analyze::AnalyzeStatement;
pub use self::begin::BeginStatement; pub use self::begin::BeginStatement;
pub use self::cancel::CancelStatement; pub use self::cancel::CancelStatement;

View file

@ -33,8 +33,11 @@ impl RemoveAccessStatement {
// Get the definition // Get the definition
let ac = txn.get_root_access(&self.name).await?; let ac = txn.get_root_access(&self.name).await?;
// Delete the definition // 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?; 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 // Clear the cache
txn.clear(); txn.clear();
// Ok all good // Ok all good
@ -46,8 +49,11 @@ impl RemoveAccessStatement {
// Get the definition // Get the definition
let ac = txn.get_ns_access(opt.ns()?, &self.name).await?; let ac = txn.get_ns_access(opt.ns()?, &self.name).await?;
// Delete the definition // 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?; 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 // Clear the cache
txn.clear(); txn.clear();
// Ok all good // Ok all good
@ -59,8 +65,12 @@ impl RemoveAccessStatement {
// Get the definition // Get the definition
let ac = txn.get_db_access(opt.ns()?, opt.db()?, &self.name).await?; let ac = txn.get_db_access(opt.ns()?, opt.db()?, &self.name).await?;
// Delete the definition // 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?; 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 // Clear the cache
txn.clear(); txn.clear();
// Ok all good // Ok all good

View file

@ -542,6 +542,15 @@ impl From<Option<Duration>> for Value {
} }
} }
impl From<Option<Datetime>> for Value {
fn from(v: Option<Datetime>) -> Self {
match v {
Some(v) => Value::from(v),
None => Value::None,
}
}
}
impl From<Id> for Value { impl From<Id> for Value {
fn from(v: Id) -> Self { fn from(v: Id) -> Self {
match v { match v {

View file

@ -70,6 +70,7 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
UniCase::ascii("ASSERT") => TokenKind::Keyword(Keyword::Assert), UniCase::ascii("ASSERT") => TokenKind::Keyword(Keyword::Assert),
UniCase::ascii("AT") => TokenKind::Keyword(Keyword::At), UniCase::ascii("AT") => TokenKind::Keyword(Keyword::At),
UniCase::ascii("AUTHENTICATE") => TokenKind::Keyword(Keyword::Authenticate), UniCase::ascii("AUTHENTICATE") => TokenKind::Keyword(Keyword::Authenticate),
UniCase::ascii("BEARER") => TokenKind::Keyword(Keyword::Bearer),
UniCase::ascii("BEFORE") => TokenKind::Keyword(Keyword::Before), UniCase::ascii("BEFORE") => TokenKind::Keyword(Keyword::Before),
UniCase::ascii("BEGIN") => TokenKind::Keyword(Keyword::Begin), UniCase::ascii("BEGIN") => TokenKind::Keyword(Keyword::Begin),
UniCase::ascii("BLANK") => TokenKind::Keyword(Keyword::Blank), UniCase::ascii("BLANK") => TokenKind::Keyword(Keyword::Blank),
@ -145,6 +146,7 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
UniCase::ascii("KILL") => TokenKind::Keyword(Keyword::Kill), UniCase::ascii("KILL") => TokenKind::Keyword(Keyword::Kill),
UniCase::ascii("LET") => TokenKind::Keyword(Keyword::Let), UniCase::ascii("LET") => TokenKind::Keyword(Keyword::Let),
UniCase::ascii("LIMIT") => TokenKind::Keyword(Keyword::Limit), UniCase::ascii("LIMIT") => TokenKind::Keyword(Keyword::Limit),
UniCase::ascii("LIST") => TokenKind::Keyword(Keyword::List),
UniCase::ascii("LIVE") => TokenKind::Keyword(Keyword::Live), UniCase::ascii("LIVE") => TokenKind::Keyword(Keyword::Live),
UniCase::ascii("LOWERCASE") => TokenKind::Keyword(Keyword::Lowercase), UniCase::ascii("LOWERCASE") => TokenKind::Keyword(Keyword::Lowercase),
UniCase::ascii("LM") => TokenKind::Keyword(Keyword::Lm), UniCase::ascii("LM") => TokenKind::Keyword(Keyword::Lm),
@ -178,6 +180,7 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
UniCase::ascii("PERMISSIONS") => TokenKind::Keyword(Keyword::Permissions), UniCase::ascii("PERMISSIONS") => TokenKind::Keyword(Keyword::Permissions),
UniCase::ascii("POSTINGS_CACHE") => TokenKind::Keyword(Keyword::PostingsCache), UniCase::ascii("POSTINGS_CACHE") => TokenKind::Keyword(Keyword::PostingsCache),
UniCase::ascii("POSTINGS_ORDER") => TokenKind::Keyword(Keyword::PostingsOrder), UniCase::ascii("POSTINGS_ORDER") => TokenKind::Keyword(Keyword::PostingsOrder),
UniCase::ascii("PRUNE") => TokenKind::Keyword(Keyword::Prune),
UniCase::ascii("PUNCT") => TokenKind::Keyword(Keyword::Punct), UniCase::ascii("PUNCT") => TokenKind::Keyword(Keyword::Punct),
UniCase::ascii("READONLY") => TokenKind::Keyword(Keyword::Readonly), UniCase::ascii("READONLY") => TokenKind::Keyword(Keyword::Readonly),
UniCase::ascii("RELATE") => TokenKind::Keyword(Keyword::Relate), UniCase::ascii("RELATE") => TokenKind::Keyword(Keyword::Relate),
@ -186,6 +189,7 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
UniCase::ascii("REMOVE") => TokenKind::Keyword(Keyword::Remove), UniCase::ascii("REMOVE") => TokenKind::Keyword(Keyword::Remove),
UniCase::ascii("REPLACE") => TokenKind::Keyword(Keyword::Replace), UniCase::ascii("REPLACE") => TokenKind::Keyword(Keyword::Replace),
UniCase::ascii("RETURN") => TokenKind::Keyword(Keyword::Return), UniCase::ascii("RETURN") => TokenKind::Keyword(Keyword::Return),
UniCase::ascii("REVOKE") => TokenKind::Keyword(Keyword::Revoke),
UniCase::ascii("ROLES") => TokenKind::Keyword(Keyword::Roles), UniCase::ascii("ROLES") => TokenKind::Keyword(Keyword::Roles),
UniCase::ascii("ROOT") => TokenKind::Keyword(Keyword::Root), UniCase::ascii("ROOT") => TokenKind::Keyword(Keyword::Root),
UniCase::ascii("KV") => TokenKind::Keyword(Keyword::Root), UniCase::ascii("KV") => TokenKind::Keyword(Keyword::Root),

View file

@ -1,5 +1,6 @@
use reblessive::Stk; use reblessive::Stk;
use crate::cnf::EXPERIMENTAL_BEARER_ACCESS;
use crate::sql::access_type::JwtAccessVerify; use crate::sql::access_type::JwtAccessVerify;
use crate::sql::index::HnswParams; use crate::sql::index::HnswParams;
use crate::{ use crate::{
@ -339,6 +340,25 @@ impl Parser<'_> {
} }
res.kind = AccessType::Record(ac); 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, _ => break,
} }
} }

View file

@ -1,11 +1,15 @@
use reblessive::Stk; use reblessive::Stk;
use crate::cnf::EXPERIMENTAL_BEARER_ACCESS;
use crate::enter_query_recursion; use crate::enter_query_recursion;
use crate::sql::block::Entry; use crate::sql::block::Entry;
use crate::sql::statements::rebuild::{RebuildIndexStatement, RebuildStatement}; use crate::sql::statements::rebuild::{RebuildIndexStatement, RebuildStatement};
use crate::sql::statements::show::{ShowSince, ShowStatement}; use crate::sql::statements::show::{ShowSince, ShowStatement};
use crate::sql::statements::sleep::SleepStatement; use crate::sql::statements::sleep::SleepStatement;
use crate::sql::statements::{ use crate::sql::statements::{
access::{
AccessStatement, AccessStatementGrant, AccessStatementList, AccessStatementRevoke, Subject,
},
KillStatement, LiveStatement, OptionStatement, SetStatement, ThrowStatement, KillStatement, LiveStatement, OptionStatement, SetStatement, ThrowStatement,
}; };
use crate::sql::{Fields, Ident, Param}; use crate::sql::{Fields, Ident, Param};
@ -81,21 +85,22 @@ impl Parser<'_> {
fn token_kind_starts_statement(kind: TokenKind) -> bool { fn token_kind_starts_statement(kind: TokenKind) -> bool {
matches!( matches!(
kind, kind,
t!("ALTER") t!("ACCESS")
| t!("ANALYZE") | t!("BEGIN") | t!("ALTER") | t!("ANALYZE")
| t!("BREAK") | t!("CANCEL") | t!("BEGIN") | t!("BREAK")
| t!("COMMIT") | t!("CONTINUE") | t!("CANCEL") | t!("COMMIT")
| t!("CREATE") | t!("DEFINE") | t!("CONTINUE") | t!("CREATE")
| t!("DELETE") | t!("FOR") | t!("DEFINE") | t!("DELETE")
| t!("IF") | t!("INFO") | t!("FOR") | t!("IF")
| t!("INSERT") | t!("KILL") | t!("INFO") | t!("INSERT")
| t!("LIVE") | t!("OPTION") | t!("KILL") | t!("LIVE")
| t!("REBUILD") | t!("RETURN") | t!("OPTION") | t!("REBUILD")
| t!("RELATE") | t!("REMOVE") | t!("RETURN") | t!("RELATE")
| t!("SELECT") | t!("LET") | t!("REMOVE") | t!("SELECT")
| t!("SHOW") | t!("SLEEP") | t!("LET") | t!("SHOW")
| t!("THROW") | t!("UPDATE") | t!("SLEEP") | t!("THROW")
| t!("UPSERT") | t!("USE") | t!("UPDATE") | t!("UPSERT")
| t!("USE")
) )
} }
@ -108,6 +113,18 @@ impl Parser<'_> {
async fn parse_stmt_inner(&mut self, ctx: &mut Stk) -> ParseResult<Statement> { async fn parse_stmt_inner(&mut self, ctx: &mut Stk) -> ParseResult<Statement> {
let token = self.peek(); let token = self.peek();
match token.kind { 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") => { t!("ALTER") => {
self.pop_peek(); self.pop_peek();
ctx.run(|ctx| self.parse_alter_stmt(ctx)).await.map(Statement::Alter) 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<AccessStatement> {
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. /// Parsers a analyze statement.
fn parse_analyze(&mut self) -> ParseResult<AnalyzeStatement> { fn parse_analyze(&mut self) -> ParseResult<AnalyzeStatement> {
expected!(self, t!("INDEX")); expected!(self, t!("INDEX"));

View file

@ -11,11 +11,13 @@ use crate::{
index::{Distance, HnswParams, MTreeParams, SearchParams, VectorType}, index::{Distance, HnswParams, MTreeParams, SearchParams, VectorType},
language::Language, language::Language,
statements::{ statements::{
access,
access::{AccessStatementGrant, AccessStatementList, AccessStatementRevoke},
analyze::AnalyzeStatement, analyze::AnalyzeStatement,
show::{ShowSince, ShowStatement}, show::{ShowSince, ShowStatement},
sleep::SleepStatement, sleep::SleepStatement,
BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement, AccessStatement, BeginStatement, BreakStatement, CancelStatement, CommitStatement,
CreateStatement, DefineAccessStatement, DefineAnalyzerStatement, ContinueStatement, CreateStatement, DefineAccessStatement, DefineAnalyzerStatement,
DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement, DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement,
DefineFunctionStatement, DefineIndexStatement, DefineNamespaceStatement, DefineFunctionStatement, DefineIndexStatement, DefineNamespaceStatement,
DefineParamStatement, DefineStatement, DefineTableStatement, DeleteStatement, 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,
}))
);
}

View file

@ -37,6 +37,7 @@ keyword! {
Assert => "ASSERT", Assert => "ASSERT",
At => "AT", At => "AT",
Authenticate => "AUTHENTICATE", Authenticate => "AUTHENTICATE",
Bearer => "BEARER",
Before => "BEFORE", Before => "BEFORE",
Begin => "BEGIN", Begin => "BEGIN",
Blank => "BLANK", Blank => "BLANK",
@ -106,6 +107,7 @@ keyword! {
Kill => "KILL", Kill => "KILL",
Let => "LET", Let => "LET",
Limit => "LIMIT", Limit => "LIMIT",
List => "LIST",
Live => "LIVE", Live => "LIVE",
Lowercase => "LOWERCASE", Lowercase => "LOWERCASE",
Lm => "LM", Lm => "LM",
@ -137,6 +139,7 @@ keyword! {
Permissions => "PERMISSIONS", Permissions => "PERMISSIONS",
PostingsCache => "POSTINGS_CACHE", PostingsCache => "POSTINGS_CACHE",
PostingsOrder => "POSTINGS_ORDER", PostingsOrder => "POSTINGS_ORDER",
Prune => "PRUNE",
Punct => "PUNCT", Punct => "PUNCT",
Readonly => "READONLY", Readonly => "READONLY",
Rebuild => "REBUILD", Rebuild => "REBUILD",
@ -145,6 +148,7 @@ keyword! {
Remove => "REMOVE", Remove => "REMOVE",
Replace => "REPLACE", Replace => "REPLACE",
Return => "RETURN", Return => "RETURN",
Revoke => "REVOKE",
Roles => "ROLES", Roles => "ROLES",
Root => "ROOT", Root => "ROOT",
Schemafull => "SCHEMAFULL", Schemafull => "SCHEMAFULL",

629
lib/tests/access.rs Normal file
View file

@ -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
)
}
}
}
}