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

View file

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

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.
pub static EXTERNAL_SORTING_BUFFER_LIMIT: Lazy<usize> =
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::split::Splits;
use crate::sql::start::Start;
use crate::sql::statements::access::AccessStatement;
use crate::sql::statements::create::CreateStatement;
use crate::sql::statements::delete::DeleteStatement;
use crate::sql::statements::insert::InsertStatement;
@ -32,6 +33,9 @@ pub(crate) enum Statement<'a> {
Relate(&'a RelateStatement),
Delete(&'a DeleteStatement),
Insert(&'a InsertStatement),
// TODO(gguillemas): Document once bearer access is no longer experimental.
#[doc(hidden)]
Access(&'a AccessStatement),
}
impl<'a> From<&'a LiveStatement> for Statement<'a> {
@ -88,6 +92,12 @@ impl<'a> From<&'a InsertStatement> for Statement<'a> {
}
}
impl<'a> From<&'a AccessStatement> for Statement<'a> {
fn from(v: &'a AccessStatement) -> Self {
Statement::Access(v)
}
}
impl<'a> fmt::Display for Statement<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
@ -100,6 +110,7 @@ impl<'a> fmt::Display for Statement<'a> {
Statement::Relate(v) => write!(f, "{v}"),
Statement::Delete(v) => write!(f, "{v}"),
Statement::Insert(v) => write!(f, "{v}"),
Statement::Access(v) => write!(f, "{v}"),
}
}
}

View file

@ -932,43 +932,67 @@ pub enum Error {
Serialization(String),
/// The requested root access method already exists
#[error("The root access method '{value}' already exists")]
#[error("The root access method '{ac}' already exists")]
AccessRootAlreadyExists {
value: String,
ac: String,
},
/// The requested namespace access method already exists
#[error("The access method '{value}' already exists in the namespace '{ns}'")]
#[error("The access method '{ac}' already exists in the namespace '{ns}'")]
AccessNsAlreadyExists {
value: String,
ac: String,
ns: String,
},
/// The requested database access method already exists
#[error("The access method '{value}' already exists in the database '{db}'")]
#[error("The access method '{ac}' already exists in the database '{db}'")]
AccessDbAlreadyExists {
value: String,
ac: String,
ns: String,
db: String,
},
/// The requested root access method does not exist
#[error("The root access method '{value}' does not exist")]
#[error("The root access method '{ac}' does not exist")]
AccessRootNotFound {
value: String,
ac: String,
},
/// The requested root access grant does not exist
#[error("The root access grant '{gr}' does not exist")]
AccessGrantRootNotFound {
ac: String,
gr: String,
},
/// The requested namespace access method does not exist
#[error("The access method '{value}' does not exist in the namespace '{ns}'")]
#[error("The access method '{ac}' does not exist in the namespace '{ns}'")]
AccessNsNotFound {
value: String,
ac: String,
ns: String,
},
/// The requested namespace access grant does not exist
#[error("The access grant '{gr}' does not exist in the namespace '{ns}'")]
AccessGrantNsNotFound {
ac: String,
gr: String,
ns: String,
},
/// The requested database access method does not exist
#[error("The access method '{value}' does not exist in the database '{db}'")]
#[error("The access method '{ac}' does not exist in the database '{db}'")]
AccessDbNotFound {
value: String,
ac: String,
ns: String,
db: String,
},
/// The requested database access grant does not exist
#[error("The access grant '{gr}' does not exist in the database '{db}'")]
AccessGrantDbNotFound {
ac: String,
gr: String,
ns: String,
db: String,
},
@ -1001,6 +1025,18 @@ pub enum Error {
#[error("This record access method does not allow signin")]
AccessRecordNoSignin,
#[error("This bearer access method requires a key to be provided")]
AccessBearerMissingKey,
#[error("This bearer access grant has an invalid format")]
AccessGrantBearerInvalid,
#[error("This access grant has an invalid subject")]
AccessGrantInvalidSubject,
#[error("This access grant has been revoked")]
AccessGrantRevoked,
/// Found a table name for the record but this is not a valid table
#[error("Found {value} for the Record ID but this is not a valid table name")]
TbInvalid {

View file

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

View file

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

View file

@ -46,6 +46,7 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy<serde_json::Value> = Lazy::new(|| {
"Event": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Field": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Index": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Access": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
// IAM resource types
"Role": {},
@ -65,14 +66,14 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy<serde_json::Value> = Lazy::new(|| {
"View": {
"appliesTo": {
"principalTypes": [ "Actor" ],
"resourceTypes": [ "Any", "Namespace", "Database", "Record", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Actor" ],
"resourceTypes": [ "Any", "Namespace", "Database", "Record", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Access", "Actor" ],
},
},
"Edit": {
"appliesTo": {
"principalTypes": [ "Actor" ],
"resourceTypes": [ "Any", "Namespace", "Database", "Record", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Actor" ],
"resourceTypes": [ "Any", "Namespace", "Database", "Record", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Access", "Actor" ],
},
},
},

View file

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

View file

@ -147,7 +147,89 @@ impl From<Claims> for Value {
continue;
}
};
out.insert(claim, claim_value);
out.insert(claim.to_owned(), claim_value);
}
}
// Return value
out.into()
}
}
impl From<&Claims> for Value {
fn from(v: &Claims) -> Value {
// Set default value
let mut out = Object::default();
// Add iss field if set
if let Some(iss) = &v.iss {
out.insert("iss".to_string(), iss.clone().into());
}
// Add sub field if set
if let Some(sub) = &v.sub {
out.insert("sub".to_string(), sub.clone().into());
}
// Add aud field if set
if let Some(aud) = &v.aud {
match aud {
Audience::Single(v) => out.insert("aud".to_string(), v.clone().into()),
Audience::Multiple(v) => out.insert("aud".to_string(), v.clone().into()),
};
}
// Add iat field if set
if let Some(iat) = v.iat {
out.insert("iat".to_string(), iat.into());
}
// Add nbf field if set
if let Some(nbf) = v.nbf {
out.insert("nbf".to_string(), nbf.into());
}
// Add exp field if set
if let Some(exp) = v.exp {
out.insert("exp".to_string(), exp.into());
}
// Add jti field if set
if let Some(jti) = &v.jti {
out.insert("jti".to_string(), jti.clone().into());
}
// Add NS field if set
if let Some(ns) = &v.ns {
out.insert("NS".to_string(), ns.clone().into());
}
// Add DB field if set
if let Some(db) = &v.db {
out.insert("DB".to_string(), db.clone().into());
}
// Add AC field if set
if let Some(ac) = &v.ac {
out.insert("AC".to_string(), ac.clone().into());
}
// Add ID field if set
if let Some(id) = &v.id {
out.insert("ID".to_string(), id.clone().into());
}
// Add RL field if set
if let Some(role) = &v.roles {
out.insert("RL".to_string(), role.clone().into());
}
// Add custom claims if set
if let Some(custom_claims) = &v.custom_claims {
for (claim, value) in custom_claims {
// Serialize the raw JSON string representing the claim value
let claim_json = match serde_json::to_string(&value) {
Ok(claim_json) => claim_json,
Err(err) => {
debug!("Failed to serialize token claim '{}': {}", claim, err);
continue;
}
};
// Parse that JSON string into the corresponding SurrealQL value
let claim_value = match json(&claim_json) {
Ok(claim_value) => claim_value,
Err(err) => {
debug!("Failed to parse token claim '{}': {}", claim, err);
continue;
}
};
out.insert(claim.to_owned(), claim_value);
}
}
// Return value

View file

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

View file

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

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::Category;
use derive::Key;
@ -23,13 +23,13 @@ pub fn new<'a>(ns: &'a str, db: &'a str, ac: &'a str) -> Ac<'a> {
}
pub fn prefix(ns: &str, db: &str) -> Vec<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
}
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
}
@ -68,7 +68,7 @@ mod tests {
"testac",
);
let enc = Ac::encode(&val).unwrap();
assert_eq!(enc, b"/*testns\x00*testdb\x00!actestac\x00");
assert_eq!(enc, b"/*testns\0*testdb\0!actestac\0");
let dec = Ac::decode(&enc).unwrap();
assert_eq!(val, dec);

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 az;
pub mod fc;

View file

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

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::Category;
use derive::Key;
@ -21,13 +21,13 @@ pub fn new<'a>(ns: &'a str, ac: &'a str) -> Ac<'a> {
}
pub fn prefix(ns: &str) -> Vec<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
}
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
}
@ -64,7 +64,20 @@ mod tests {
);
let enc = Ac::encode(&val).unwrap();
assert_eq!(enc, b"/*testns\0!actestac\0");
let dec = Ac::decode(&enc).unwrap();
assert_eq!(val, dec);
}
#[test]
fn test_prefix() {
let val = super::prefix("testns");
assert_eq!(val, b"/*testns\0!ac\0");
}
#[test]
fn test_suffix() {
let val = super::suffix("testns");
assert_eq!(val, b"/*testns\0!ac\xff");
}
}

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 db;
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::Category;
use derive::Key;
@ -19,13 +19,13 @@ pub fn new(ac: &str) -> Ac<'_> {
}
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
}
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
}

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 nd;
pub mod ni;

View file

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

View file

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

View file

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

View file

@ -9,13 +9,22 @@ use std::fmt;
use std::fmt::Display;
/// The type of access methods available
#[revisioned(revision = 1)]
#[revisioned(revision = 2)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub enum AccessType {
Record(RecordAccess),
Jwt(JwtAccess),
// TODO(gguillemas): Document once bearer access is no longer experimental.
#[doc(hidden)]
#[revision(start = 2)]
Bearer(BearerAccess),
}
// Allows retrieving the JWT configuration for any access type.
pub trait Jwt {
fn jwt(&self) -> &JwtAccess;
}
impl Default for AccessType {
@ -27,6 +36,16 @@ impl Default for AccessType {
}
}
impl Jwt for AccessType {
fn jwt(&self) -> &JwtAccess {
match self {
AccessType::Record(at) => at.jwt(),
AccessType::Jwt(at) => at.jwt(),
AccessType::Bearer(at) => at.jwt(),
}
}
}
impl Display for AccessType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
@ -43,6 +62,12 @@ impl Display for AccessType {
}
write!(f, " WITH JWT {}", ac.jwt)?;
}
AccessType::Bearer(ac) => {
write!(f, "BEARER")?;
if let BearerAccessLevel::Record = ac.level {
write!(f, " FOR RECORD")?;
}
}
}
Ok(())
}
@ -61,11 +86,21 @@ impl InfoStructure for AccessType {
"signup".to_string(), if let Some(v) = v.signup => v.structure(),
"signin".to_string(), if let Some(v) = v.signin => v.structure(),
}),
AccessType::Bearer(ac) => Value::from(map! {
"kind".to_string() => "BEARER".into(),
"level".to_string() => match ac.level {
BearerAccessLevel::Record => "RECORD",
BearerAccessLevel::User => "USER",
}.into(),
"jwt".to_string() => ac.jwt.structure(),
}),
}
}
}
impl AccessType {
// TODO(gguillemas): Document once bearer access is no longer experimental.
#[doc(hidden)]
/// Returns whether or not the access method can issue non-token grants
/// In this context, token refers exclusively to JWT
#[allow(unreachable_patterns)]
@ -73,8 +108,7 @@ impl AccessType {
match self {
// The grants for JWT and record access methods are JWT
AccessType::Jwt(_) | AccessType::Record(_) => false,
// TODO(gguillemas): This arm should be reachable by the bearer access method
_ => unreachable!(),
AccessType::Bearer(_) => true,
}
}
/// Returns whether or not the access method can issue tokens
@ -118,6 +152,12 @@ impl Default for JwtAccess {
}
}
impl Jwt for JwtAccess {
fn jwt(&self) -> &JwtAccess {
self
}
}
impl Display for JwtAccess {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.verify {
@ -296,3 +336,45 @@ impl Default for RecordAccess {
}
}
}
impl Jwt for RecordAccess {
fn jwt(&self) -> &JwtAccess {
&self.jwt
}
}
// TODO(gguillemas): Document once bearer access is no longer experimental.
#[doc(hidden)]
#[revisioned(revision = 1)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct BearerAccess {
pub level: BearerAccessLevel,
pub jwt: JwtAccess,
}
impl Default for BearerAccess {
fn default() -> Self {
Self {
level: BearerAccessLevel::User,
jwt: JwtAccess {
..Default::default()
},
}
}
}
impl Jwt for BearerAccess {
fn jwt(&self) -> &JwtAccess {
&self.jwt
}
}
#[revisioned(revision = 1)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub enum BearerAccessLevel {
Record,
User,
}

View file

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

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

View file

@ -1,3 +1,4 @@
pub(crate) mod access;
pub(crate) mod alter;
pub(crate) mod analyze;
pub(crate) mod begin;
@ -28,6 +29,9 @@ pub(crate) mod update;
pub(crate) mod upsert;
pub(crate) mod r#use;
// TODO(gguillemas): Document once bearer access is no longer experimental.
#[doc(hidden)]
pub use self::access::{AccessGrant, AccessStatement};
pub use self::analyze::AnalyzeStatement;
pub use self::begin::BeginStatement;
pub use self::cancel::CancelStatement;

View file

@ -33,8 +33,11 @@ impl RemoveAccessStatement {
// Get the definition
let ac = txn.get_root_access(&self.name).await?;
// Delete the definition
let key = crate::key::root::ac::new(&ac.name);
let key = crate::key::root::access::ac::new(&ac.name);
txn.del(key).await?;
// Delete any associated data including access grants.
let key = crate::key::root::access::all::new(&ac.name);
txn.delp(key).await?;
// Clear the cache
txn.clear();
// Ok all good
@ -46,8 +49,11 @@ impl RemoveAccessStatement {
// Get the definition
let ac = txn.get_ns_access(opt.ns()?, &self.name).await?;
// Delete the definition
let key = crate::key::namespace::ac::new(opt.ns()?, &ac.name);
let key = crate::key::namespace::access::ac::new(opt.ns()?, &ac.name);
txn.del(key).await?;
// Delete any associated data including access grants.
let key = crate::key::namespace::access::all::new(opt.ns()?, &ac.name);
txn.delp(key).await?;
// Clear the cache
txn.clear();
// Ok all good
@ -59,8 +65,12 @@ impl RemoveAccessStatement {
// Get the definition
let ac = txn.get_db_access(opt.ns()?, opt.db()?, &self.name).await?;
// Delete the definition
let key = crate::key::database::ac::new(opt.ns()?, opt.db()?, &ac.name);
let key = crate::key::database::access::ac::new(opt.ns()?, opt.db()?, &ac.name);
txn.del(key).await?;
// Delete any associated data including access grants.
let key =
crate::key::database::access::all::new(opt.ns()?, opt.db()?, &ac.name);
txn.delp(key).await?;
// Clear the cache
txn.clear();
// Ok all good

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

View file

@ -1,5 +1,6 @@
use reblessive::Stk;
use crate::cnf::EXPERIMENTAL_BEARER_ACCESS;
use crate::sql::access_type::JwtAccessVerify;
use crate::sql::index::HnswParams;
use crate::{
@ -339,6 +340,25 @@ impl Parser<'_> {
}
res.kind = AccessType::Record(ac);
}
t!("BEARER") => {
// TODO(gguillemas): Remove this once bearer access is no longer experimental.
if !*EXPERIMENTAL_BEARER_ACCESS {
unexpected!(
self,
t!("BEARER"),
"the experimental bearer access feature to be enabled"
);
}
self.pop_peek();
let mut ac = access_type::BearerAccess {
..Default::default()
};
if self.eat(t!("WITH")) {
expected!(self, t!("JWT"));
ac.jwt = self.parse_jwt()?;
}
res.kind = AccessType::Bearer(ac);
}
_ => break,
}
}

View file

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

View file

@ -11,11 +11,13 @@ use crate::{
index::{Distance, HnswParams, MTreeParams, SearchParams, VectorType},
language::Language,
statements::{
access,
access::{AccessStatementGrant, AccessStatementList, AccessStatementRevoke},
analyze::AnalyzeStatement,
show::{ShowSince, ShowStatement},
sleep::SleepStatement,
BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement,
CreateStatement, DefineAccessStatement, DefineAnalyzerStatement,
AccessStatement, BeginStatement, BreakStatement, CancelStatement, CommitStatement,
ContinueStatement, CreateStatement, DefineAccessStatement, DefineAnalyzerStatement,
DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement,
DefineFunctionStatement, DefineIndexStatement, DefineNamespaceStatement,
DefineParamStatement, DefineStatement, DefineTableStatement, DeleteStatement,
@ -2419,3 +2421,41 @@ fn parse_upsert() {
})
);
}
#[test]
fn parse_access_grant() {
let res = test_parse!(parse_stmt, r#"ACCESS a ON NAMESPACE GRANT FOR USER b"#).unwrap();
assert_eq!(
res,
Statement::Access(AccessStatement::Grant(AccessStatementGrant {
ac: Ident("a".to_string()),
base: Some(Base::Ns),
subject: Some(access::Subject::User(Ident("b".to_string()))),
}))
);
}
#[test]
fn parse_access_revoke() {
let res = test_parse!(parse_stmt, r#"ACCESS a ON DATABASE REVOKE b"#).unwrap();
assert_eq!(
res,
Statement::Access(AccessStatement::Revoke(AccessStatementRevoke {
ac: Ident("a".to_string()),
base: Some(Base::Db),
gr: Ident("b".to_string()),
}))
);
}
#[test]
fn parse_access_list() {
let res = test_parse!(parse_stmt, r#"ACCESS a LIST"#).unwrap();
assert_eq!(
res,
Statement::Access(AccessStatement::List(AccessStatementList {
ac: Ident("a".to_string()),
base: None,
}))
);
}

View file

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

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