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:
parent
a87433c4d3
commit
c3d788ff4a
46 changed files with 4928 additions and 170 deletions
5
Cargo.lock
generated
5
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" ],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
60
core/src/key/database/access/all.rs
Normal file
60
core/src/key/database/access/all.rs
Normal 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);
|
||||
}
|
||||
}
|
93
core/src/key/database/access/gr.rs
Normal file
93
core/src/key/database/access/gr.rs
Normal 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");
|
||||
}
|
||||
}
|
3
core/src/key/database/access/mod.rs
Normal file
3
core/src/key/database/access/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod ac;
|
||||
pub mod all;
|
||||
pub mod gr;
|
|
@ -1,4 +1,4 @@
|
|||
pub mod ac;
|
||||
pub mod access;
|
||||
pub mod all;
|
||||
pub mod az;
|
||||
pub mod fc;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
55
core/src/key/namespace/access/all.rs
Normal file
55
core/src/key/namespace/access/all.rs
Normal 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);
|
||||
}
|
||||
}
|
88
core/src/key/namespace/access/gr.rs
Normal file
88
core/src/key/namespace/access/gr.rs
Normal 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");
|
||||
}
|
||||
}
|
3
core/src/key/namespace/access/mod.rs
Normal file
3
core/src/key/namespace/access/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod ac;
|
||||
pub mod all;
|
||||
pub mod gr;
|
|
@ -1,4 +1,4 @@
|
|||
pub mod ac;
|
||||
pub mod access;
|
||||
pub mod all;
|
||||
pub mod db;
|
||||
pub mod di;
|
||||
|
|
|
@ -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
|
||||
}
|
50
core/src/key/root/access/all.rs
Normal file
50
core/src/key/root/access/all.rs
Normal 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);
|
||||
}
|
||||
}
|
83
core/src/key/root/access/gr.rs
Normal file
83
core/src/key/root/access/gr.rs
Normal 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");
|
||||
}
|
||||
}
|
3
core/src/key/root/access/mod.rs
Normal file
3
core/src/key/root/access/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod ac;
|
||||
pub mod all;
|
||||
pub mod gr;
|
|
@ -1,4 +1,4 @@
|
|||
pub mod ac;
|
||||
pub mod access;
|
||||
pub mod all;
|
||||
pub mod nd;
|
||||
pub mod ni;
|
||||
|
|
|
@ -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]> {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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}"),
|
||||
|
|
628
core/src/sql/statements/access.rs
Normal file
628
core/src/sql/statements/access.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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
629
lib/tests/access.rs
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue