Consolidate authentication methods (#3988)

Co-authored-by: Micha de Vries <micha@devrie.sh>
This commit is contained in:
Gerard Guillemas Martos 2024-05-22 15:57:25 +02:00 committed by GitHub
parent e37a6fb18b
commit e38b891e62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
93 changed files with 3581 additions and 2214 deletions

View file

@ -21,7 +21,7 @@ pub static MAX_COMPUTATION_DEPTH: Lazy<u32> =
lazy_env_parse!("SURREAL_MAX_COMPUTATION_DEPTH", u32, 120); lazy_env_parse!("SURREAL_MAX_COMPUTATION_DEPTH", u32, 120);
/// Specifies the names of parameters which can not be specified in a query. /// Specifies the names of parameters which can not be specified in a query.
pub const PROTECTED_PARAM_NAMES: &[&str] = &["auth", "scope", "token", "session"]; pub const PROTECTED_PARAM_NAMES: &[&str] = &["access", "auth", "token", "session"];
/// The characters which are supported in server record IDs. /// The characters which are supported in server record IDs.
pub const ID_CHARS: [char; 36] = [ pub const ID_CHARS: [char; 36] = [
@ -35,9 +35,9 @@ pub const SERVER_NAME: &str = "SurrealDB";
/// Datastore processor batch size for scan operations /// Datastore processor batch size for scan operations
pub const PROCESSOR_BATCH_SIZE: u32 = 50; pub const PROCESSOR_BATCH_SIZE: u32 = 50;
/// Forward all signup/signin query errors to a client trying authenticate to a scope. Do not use in production. /// Forward all signup/signin query errors to a client performing record access. Do not use in production.
pub static INSECURE_FORWARD_SCOPE_ERRORS: Lazy<bool> = pub static INSECURE_FORWARD_RECORD_ACCESS_ERRORS: Lazy<bool> =
lazy_env_parse!("SURREAL_INSECURE_FORWARD_SCOPE_ERRORS", bool, false); lazy_env_parse!("SURREAL_INSECURE_FORWARD_RECORD_ACCESS_ERRORS", bool, false);
#[cfg(any( #[cfg(any(
feature = "kv-surrealkv", feature = "kv-surrealkv",

View file

@ -405,9 +405,10 @@ impl Options {
self.valid_for_db()?; self.valid_for_db()?;
res.on_db(self.ns(), self.db()) res.on_db(self.ns(), self.db())
} }
Base::Sc(sc) => { // TODO(gguillemas): This variant is kept in 2.0.0 for backward compatibility. Drop in 3.0.0.
self.valid_for_db()?; Base::Sc(_) => {
res.on_scope(self.ns(), self.db(), sc) // We should not get here, the scope base is only used in parsing for backward compatibility.
return Err(Error::InvalidAuth);
} }
}; };

View file

@ -23,12 +23,12 @@ pub struct Session {
pub ns: Option<String>, pub ns: Option<String>,
/// The currently selected database /// The currently selected database
pub db: Option<String>, pub db: Option<String>,
/// The currently selected authentication scope /// The current access method
pub sc: Option<String>, pub ac: Option<String>,
/// The current scope authentication token /// The current authentication token
pub tk: Option<Value>, pub tk: Option<Value>,
/// The current scope authentication data /// The current record authentication data
pub sd: Option<Value>, pub rd: Option<Value>,
/// The current expiration time of the session /// The current expiration time of the session
pub exp: Option<i64>, pub exp: Option<i64>,
} }
@ -46,9 +46,9 @@ impl Session {
self self
} }
/// Set the selected database for the session /// Set the selected access method for the session
pub fn with_sc(mut self, sc: &str) -> Session { pub fn with_ac(mut self, ac: &str) -> Session {
self.sc = Some(sc.to_owned()); self.ac = Some(ac.to_owned());
self self
} }
@ -84,26 +84,26 @@ impl Session {
/// Convert a session into a runtime /// Convert a session into a runtime
pub(crate) fn context<'a>(&self, mut ctx: Context<'a>) -> Context<'a> { pub(crate) fn context<'a>(&self, mut ctx: Context<'a>) -> Context<'a> {
// Add scope auth data // Add access method data
let val: Value = self.sd.to_owned().into(); let val: Value = self.ac.to_owned().into();
ctx.add_value("access", val);
// Add record access data
let val: Value = self.rd.to_owned().into();
ctx.add_value("auth", val); ctx.add_value("auth", val);
// Add scope data
let val: Value = self.sc.to_owned().into();
ctx.add_value("scope", val);
// Add token data // Add token data
let val: Value = self.tk.to_owned().into(); let val: Value = self.tk.to_owned().into();
ctx.add_value("token", val); ctx.add_value("token", val);
// Add session value // Add session value
let val: Value = Value::from(map! { let val: Value = Value::from(map! {
"ac".to_string() => self.ac.to_owned().into(),
"exp".to_string() => self.exp.to_owned().into(),
"db".to_string() => self.db.to_owned().into(), "db".to_string() => self.db.to_owned().into(),
"id".to_string() => self.id.to_owned().into(), "id".to_string() => self.id.to_owned().into(),
"ip".to_string() => self.ip.to_owned().into(), "ip".to_string() => self.ip.to_owned().into(),
"ns".to_string() => self.ns.to_owned().into(), "ns".to_string() => self.ns.to_owned().into(),
"or".to_string() => self.or.to_owned().into(), "or".to_string() => self.or.to_owned().into(),
"sc".to_string() => self.sc.to_owned().into(), "rd".to_string() => self.rd.to_owned().into(),
"sd".to_string() => self.sd.to_owned().into(),
"tk".to_string() => self.tk.to_owned().into(), "tk".to_string() => self.tk.to_owned().into(),
"exp".to_string() => self.exp.to_owned().into(),
}); });
ctx.add_value("session", val); ctx.add_value("session", val);
// Output context // Output context
@ -133,19 +133,19 @@ impl Session {
sess sess
} }
/// Create a scoped session for a given NS and DB /// Create a record user session for a given NS and DB
pub fn for_scope(ns: &str, db: &str, sc: &str, rid: Value) -> Session { pub fn for_record(ns: &str, db: &str, ac: &str, rid: Value) -> Session {
Session { Session {
au: Arc::new(Auth::for_sc(rid.to_string(), ns, db, sc)), ac: Some(ac.to_owned()),
au: Arc::new(Auth::for_record(rid.to_string(), ns, db, ac)),
rt: false, rt: false,
ip: None, ip: None,
or: None, or: None,
id: None, id: None,
ns: Some(ns.to_owned()), ns: Some(ns.to_owned()),
db: Some(db.to_owned()), db: Some(db.to_owned()),
sc: Some(sc.to_owned()),
tk: None, tk: None,
sd: Some(rid), rd: Some(rid),
exp: None, exp: None,
} }
} }

View file

@ -7,9 +7,9 @@ use crate::doc::CursorDoc;
use crate::doc::Document; use crate::doc::Document;
use crate::err::Error; use crate::err::Error;
use crate::fflags::FFLAGS; use crate::fflags::FFLAGS;
use crate::sql::paths::AC;
use crate::sql::paths::META; use crate::sql::paths::META;
use crate::sql::paths::SC; use crate::sql::paths::RD;
use crate::sql::paths::SD;
use crate::sql::paths::TK; use crate::sql::paths::TK;
use crate::sql::permission::Permission; use crate::sql::permission::Permission;
use crate::sql::statements::LiveStatement; use crate::sql::statements::LiveStatement;
@ -161,8 +161,8 @@ impl<'a> Document<'a> {
// This ensures that we are using the session // This ensures that we are using the session
// of the user who created the LIVE query. // of the user who created the LIVE query.
let mut lqctx = Context::background(); let mut lqctx = Context::background();
lqctx.add_value("auth", sess.pick(SD.as_ref())); lqctx.add_value("access", sess.pick(AC.as_ref()));
lqctx.add_value("scope", sess.pick(SC.as_ref())); lqctx.add_value("auth", sess.pick(RD.as_ref()));
lqctx.add_value("token", sess.pick(TK.as_ref())); lqctx.add_value("token", sess.pick(TK.as_ref()));
lqctx.add_value("session", sess); lqctx.add_value("session", sess);
// We need to create a new options which we will // We need to create a new options which we will

View file

@ -299,9 +299,9 @@ pub enum Error {
value: String, value: String,
}, },
/// The requested namespace token does not exist /// The requested namespace access method does not exist
#[error("The namespace token '{value}' does not exist")] #[error("The namespace access method '{value}' does not exist")]
NtNotFound { NaNotFound {
value: String, value: String,
}, },
@ -317,9 +317,9 @@ pub enum Error {
value: String, value: String,
}, },
/// The requested database token does not exist /// The requested database access method does not exist
#[error("The database token '{value}' does not exist")] #[error("The database access method '{value}' does not exist")]
DtNotFound { DaNotFound {
value: String, value: String,
}, },
@ -353,12 +353,6 @@ pub enum Error {
value: String, value: String,
}, },
/// The requested scope does not exist
#[error("The scope '{value}' does not exist")]
ScNotFound {
value: String,
},
// The cluster node already exists // The cluster node already exists
#[error("The node '{value}' already exists")] #[error("The node '{value}' already exists")]
ClAlreadyExists { ClAlreadyExists {
@ -371,12 +365,6 @@ pub enum Error {
value: String, value: String,
}, },
/// The requested scope token does not exist
#[error("The scope token '{value}' does not exist")]
StNotFound {
value: String,
},
/// The requested param does not exist /// The requested param does not exist
#[error("The param '${value}' does not exist")] #[error("The param '${value}' does not exist")]
PaNotFound { PaNotFound {
@ -764,15 +752,6 @@ pub enum Error {
#[error("The signin query failed")] #[error("The signin query failed")]
SigninQueryFailed, SigninQueryFailed,
#[error("This scope does not allow signup")]
ScopeNoSignup,
#[error("This scope does not allow signin")]
ScopeNoSignin,
#[error("The scope does not exist")]
NoScopeFound,
#[error("Username or Password was not provided")] #[error("Username or Password was not provided")]
MissingUserOrPass, MissingUserOrPass,
@ -864,12 +843,6 @@ pub enum Error {
value: String, value: String,
}, },
/// The requested scope already exists
#[error("The scope '{value}' already exists")]
ScAlreadyExists {
value: String,
},
/// The requested table already exists /// The requested table already exists
#[error("The table '{value}' already exists")] #[error("The table '{value}' already exists")]
TbAlreadyExists { TbAlreadyExists {
@ -888,12 +861,6 @@ pub enum Error {
value: String, value: String,
}, },
/// The requested scope token already exists
#[error("The scope token '{value}' already exists")]
StAlreadyExists {
value: String,
},
/// The requested user already exists /// The requested user already exists
#[error("The user '{value}' already exists")] #[error("The user '{value}' already exists")]
UserRootAlreadyExists { UserRootAlreadyExists {
@ -921,14 +888,6 @@ pub enum Error {
#[error("The session has expired")] #[error("The session has expired")]
ExpiredSession, ExpiredSession,
/// The session has an invalid duration
#[error("The session has an invalid duration")]
InvalidSessionDuration,
/// The session has an invalid expiration
#[error("The session has an invalid expiration")]
InvalidSessionExpiration,
/// A node task has failed /// A node task has failed
#[error("A node task has failed: {0}")] #[error("A node task has failed: {0}")]
NodeAgent(&'static str), NodeAgent(&'static str),
@ -940,6 +899,70 @@ pub enum Error {
/// The supplied type could not be serialiazed into `sql::Value` /// The supplied type could not be serialiazed into `sql::Value`
#[error("Serialization error: {0}")] #[error("Serialization error: {0}")]
Serialization(String), Serialization(String),
/// The requested root access method already exists
#[error("The root access method '{value}' already exists")]
AccessRootAlreadyExists {
value: String,
},
/// The requested namespace access method already exists
#[error("The namespace access method '{value}' already exists")]
AccessNsAlreadyExists {
value: String,
},
/// The requested database access method already exists
#[error("The database access method '{value}' already exists")]
AccessDbAlreadyExists {
value: String,
},
/// The requested root access method does not exist
#[error("The root access method '{value}' does not exist")]
AccessRootNotFound {
value: String,
},
/// The requested namespace access method does not exist
#[error("The namespace access method '{value}' does not exist")]
AccessNsNotFound {
value: String,
},
/// The requested database access method does not exist
#[error("The database access method '{value}' does not exist")]
AccessDbNotFound {
value: String,
},
/// The access method cannot be defined on the requested level
#[error("The access method cannot be defined on the requested level")]
AccessLevelMismatch,
#[error("The access method cannot be used in the requested operation")]
AccessMethodMismatch,
#[error("The access method does not exist")]
AccessNotFound,
#[error("This access method has an invalid duration")]
AccessInvalidDuration,
#[error("This access method results in an invalid expiration")]
AccessInvalidExpiration,
#[error("The record access signup query failed")]
AccessRecordSignupQueryFailed,
#[error("The record access signin query failed")]
AccessRecordSigninQueryFailed,
#[error("This record access method does not allow signup")]
AccessRecordNoSignup,
#[error("This record access method does not allow signin")]
AccessRecordNoSignin,
} }
impl From<Error> for String { impl From<Error> for String {

View file

@ -230,13 +230,13 @@ pub fn synchronous(ctx: &Context<'_>, name: &str, args: Vec<Value>) -> Result<Va
"rand::uuid::v7" => rand::uuid::v7, "rand::uuid::v7" => rand::uuid::v7,
"rand::uuid" => rand::uuid, "rand::uuid" => rand::uuid,
// //
"session::ac" => session::ac(ctx),
"session::db" => session::db(ctx), "session::db" => session::db(ctx),
"session::id" => session::id(ctx), "session::id" => session::id(ctx),
"session::ip" => session::ip(ctx), "session::ip" => session::ip(ctx),
"session::ns" => session::ns(ctx), "session::ns" => session::ns(ctx),
"session::origin" => session::origin(ctx), "session::origin" => session::origin(ctx),
"session::sc" => session::sc(ctx), "session::rd" => session::rd(ctx),
"session::sd" => session::sd(ctx),
"session::token" => session::token(ctx), "session::token" => session::token(ctx),
// //
"string::concat" => string::concat, "string::concat" => string::concat,

View file

@ -12,7 +12,7 @@ impl_module_def!(
"ip" => run, "ip" => run,
"ns" => run, "ns" => run,
"origin" => run, "origin" => run,
"sc" => run, "ac" => run,
"sd" => run, "rd" => run,
"token" => run "token" => run
); );

View file

@ -1,15 +1,19 @@
use crate::ctx::Context; use crate::ctx::Context;
use crate::err::Error; use crate::err::Error;
use crate::sql::paths::AC;
use crate::sql::paths::DB; use crate::sql::paths::DB;
use crate::sql::paths::ID; use crate::sql::paths::ID;
use crate::sql::paths::IP; use crate::sql::paths::IP;
use crate::sql::paths::NS; use crate::sql::paths::NS;
use crate::sql::paths::OR; use crate::sql::paths::OR;
use crate::sql::paths::SC; use crate::sql::paths::RD;
use crate::sql::paths::SD;
use crate::sql::paths::TK; use crate::sql::paths::TK;
use crate::sql::value::Value; use crate::sql::value::Value;
pub fn ac(ctx: &Context, _: ()) -> Result<Value, Error> {
ctx.value("session").unwrap_or(&Value::None).pick(AC.as_ref()).ok()
}
pub fn db(ctx: &Context, _: ()) -> Result<Value, Error> { pub fn db(ctx: &Context, _: ()) -> Result<Value, Error> {
ctx.value("session").unwrap_or(&Value::None).pick(DB.as_ref()).ok() ctx.value("session").unwrap_or(&Value::None).pick(DB.as_ref()).ok()
} }
@ -30,12 +34,8 @@ pub fn origin(ctx: &Context, _: ()) -> Result<Value, Error> {
ctx.value("session").unwrap_or(&Value::None).pick(OR.as_ref()).ok() ctx.value("session").unwrap_or(&Value::None).pick(OR.as_ref()).ok()
} }
pub fn sc(ctx: &Context, _: ()) -> Result<Value, Error> { pub fn rd(ctx: &Context, _: ()) -> Result<Value, Error> {
ctx.value("session").unwrap_or(&Value::None).pick(SC.as_ref()).ok() ctx.value("session").unwrap_or(&Value::None).pick(RD.as_ref()).ok()
}
pub fn sd(ctx: &Context, _: ()) -> Result<Value, Error> {
ctx.value("session").unwrap_or(&Value::None).pick(SD.as_ref()).ok()
} }
pub fn token(ctx: &Context, _: ()) -> Result<Value, Error> { pub fn token(ctx: &Context, _: ()) -> Result<Value, Error> {

View file

@ -1,4 +1,4 @@
use crate::sql::statements::{DefineTokenStatement, DefineUserStatement}; use crate::sql::statements::{DefineAccessStatement, DefineUserStatement};
use revision::revisioned; use revision::revisioned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -49,9 +49,9 @@ impl Auth {
matches!(self.level(), Level::Database(_, _)) matches!(self.level(), Level::Database(_, _))
} }
/// Check if the current level is Scope /// Check if the current level is Record
pub fn is_scope(&self) -> bool { pub fn is_record(&self) -> bool {
matches!(self.level(), Level::Scope(_, _, _)) matches!(self.level(), Level::Record(_, _, _))
} }
/// System Auth helpers /// System Auth helpers
@ -70,8 +70,8 @@ impl Auth {
Self::new(Actor::new("system_auth".into(), vec![role], (ns, db).into())) Self::new(Actor::new("system_auth".into(), vec![role], (ns, db).into()))
} }
pub fn for_sc(rid: String, ns: &str, db: &str, sc: &str) -> Self { pub fn for_record(rid: String, ns: &str, db: &str, ac: &str) -> Self {
Self::new(Actor::new(rid, vec![], (ns, db, sc).into())) Self::new(Actor::new(rid.to_string(), vec![], (ns, db, ac).into()))
} }
// //
@ -95,8 +95,8 @@ impl std::convert::From<(&DefineUserStatement, Level)> for Auth {
} }
} }
impl std::convert::From<(&DefineTokenStatement, Level)> for Auth { impl std::convert::From<(&DefineAccessStatement, Level)> for Auth {
fn from(val: (&DefineTokenStatement, Level)) -> Self { fn from(val: (&DefineAccessStatement, Level)) -> Self {
Self::new((val.0, val.1).into()) Self::new((val.0, val.1).into())
} }
} }

View file

@ -6,7 +6,7 @@ use std::sync::Arc;
pub fn clear(session: &mut Session) -> Result<(), Error> { pub fn clear(session: &mut Session) -> Result<(), Error> {
session.au = Arc::new(Auth::default()); session.au = Arc::new(Auth::default());
session.tk = None; session.tk = None;
session.sc = None; session.ac = None;
session.sd = None; session.rd = None;
Ok(()) Ok(())
} }

View file

@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use super::{Level, Resource, ResourceKind}; use super::{Level, Resource, ResourceKind};
use crate::iam::Role; use crate::iam::Role;
use crate::sql::statements::{DefineTokenStatement, DefineUserStatement}; use crate::sql::statements::{DefineAccessStatement, DefineUserStatement};
// //
// User // User
@ -124,8 +124,8 @@ impl std::convert::From<(&DefineUserStatement, Level)> for Actor {
} }
} }
impl std::convert::From<(&DefineTokenStatement, Level)> for Actor { impl std::convert::From<(&DefineAccessStatement, Level)> for Actor {
fn from(val: (&DefineTokenStatement, Level)) -> Self { fn from(val: (&DefineAccessStatement, Level)) -> Self {
Self::new(val.0.name.to_string(), Vec::default(), val.1) Self::new(val.0.name.to_string(), Vec::default(), val.1)
} }
} }

View file

@ -17,7 +17,7 @@ pub enum Level {
Root, Root,
Namespace(String), Namespace(String),
Database(String, String), Database(String, String),
Scope(String, String, String), Record(String, String, String),
} }
impl std::fmt::Display for Level { impl std::fmt::Display for Level {
@ -27,7 +27,7 @@ impl std::fmt::Display for Level {
Level::Root => write!(f, "/"), Level::Root => write!(f, "/"),
Level::Namespace(ns) => write!(f, "/ns:{ns}/"), Level::Namespace(ns) => write!(f, "/ns:{ns}/"),
Level::Database(ns, db) => write!(f, "/ns:{ns}/db:{db}/"), Level::Database(ns, db) => write!(f, "/ns:{ns}/db:{db}/"),
Level::Scope(ns, db, scope) => write!(f, "/ns:{ns}/db:{db}/scope:{scope}/"), Level::Record(ns, db, id) => write!(f, "/ns:{ns}/db:{db}/id:{id}/"),
} }
} }
} }
@ -39,7 +39,7 @@ impl Level {
Level::Root => "Root", Level::Root => "Root",
Level::Namespace(_) => "Namespace", Level::Namespace(_) => "Namespace",
Level::Database(_, _) => "Database", Level::Database(_, _) => "Database",
Level::Scope(_, _, _) => "Scope", Level::Record(_, _, _) => "Record",
} }
} }
@ -47,7 +47,7 @@ impl Level {
match self { match self {
Level::Namespace(ns) => Some(ns), Level::Namespace(ns) => Some(ns),
Level::Database(ns, _) => Some(ns), Level::Database(ns, _) => Some(ns),
Level::Scope(ns, _, _) => Some(ns), Level::Record(ns, _, _) => Some(ns),
_ => None, _ => None,
} }
} }
@ -55,14 +55,14 @@ impl Level {
pub fn db(&self) -> Option<&str> { pub fn db(&self) -> Option<&str> {
match self { match self {
Level::Database(_, db) => Some(db), Level::Database(_, db) => Some(db),
Level::Scope(_, db, _) => Some(db), Level::Record(_, db, _) => Some(db),
_ => None, _ => None,
} }
} }
pub fn scope(&self) -> Option<&str> { pub fn id(&self) -> Option<&str> {
match self { match self {
Level::Scope(_, _, scope) => Some(scope), Level::Record(_, _, id) => Some(id),
_ => None, _ => None,
} }
} }
@ -73,7 +73,7 @@ impl Level {
Level::Root => None, Level::Root => None,
Level::Namespace(_) => Some(Level::Root), Level::Namespace(_) => Some(Level::Root),
Level::Database(ns, _) => Some(Level::Namespace(ns.to_owned())), Level::Database(ns, _) => Some(Level::Namespace(ns.to_owned())),
Level::Scope(ns, db, _) => Some(Level::Database(ns.to_owned(), db.to_owned())), Level::Record(ns, db, _) => Some(Level::Database(ns.to_owned(), db.to_owned())),
} }
} }
@ -90,8 +90,8 @@ impl Level {
attrs.insert("db".into(), RestrictedExpression::new_string(db.to_owned())); attrs.insert("db".into(), RestrictedExpression::new_string(db.to_owned()));
} }
if let Some(scope) = self.scope() { if let Some(id) = self.id() {
attrs.insert("scope".into(), RestrictedExpression::new_string(scope.to_owned())); attrs.insert("id".into(), RestrictedExpression::new_string(id.to_owned()));
} }
attrs attrs
@ -139,8 +139,8 @@ impl From<(&str, &str)> for Level {
} }
impl From<(&str, &str, &str)> for Level { impl From<(&str, &str, &str)> for Level {
fn from((ns, db, sc): (&str, &str, &str)) -> Self { fn from((ns, db, id): (&str, &str, &str)) -> Self {
Level::Scope(ns.to_owned(), db.to_owned(), sc.to_owned()) Level::Record(ns.to_owned(), db.to_owned(), id.to_owned())
} }
} }
@ -150,7 +150,7 @@ impl From<(Option<&str>, Option<&str>, Option<&str>)> for Level {
(None, None, None) => ().into(), (None, None, None) => ().into(),
(Some(ns), None, None) => (ns,).into(), (Some(ns), None, None) => (ns,).into(),
(Some(ns), Some(db), None) => (ns, db).into(), (Some(ns), Some(db), None) => (ns, db).into(),
(Some(ns), Some(db), Some(scope)) => (ns, db, scope).into(), (Some(ns), Some(db), Some(id)) => (ns, db, id).into(),
_ => Level::No, _ => Level::No,
} }
} }

View file

@ -18,7 +18,7 @@ pub enum ResourceKind {
Any, Any,
Namespace, Namespace,
Database, Database,
Scope, Record,
Table, Table,
Document, Document,
Option, Option,
@ -40,7 +40,7 @@ impl std::fmt::Display for ResourceKind {
ResourceKind::Any => write!(f, "Any"), ResourceKind::Any => write!(f, "Any"),
ResourceKind::Namespace => write!(f, "Namespace"), ResourceKind::Namespace => write!(f, "Namespace"),
ResourceKind::Database => write!(f, "Database"), ResourceKind::Database => write!(f, "Database"),
ResourceKind::Scope => write!(f, "Scope"), ResourceKind::Record => write!(f, "Record"),
ResourceKind::Table => write!(f, "Table"), ResourceKind::Table => write!(f, "Table"),
ResourceKind::Document => write!(f, "Document"), ResourceKind::Document => write!(f, "Document"),
ResourceKind::Option => write!(f, "Option"), ResourceKind::Option => write!(f, "Option"),
@ -74,8 +74,8 @@ impl ResourceKind {
self.on_level((ns, db).into()) self.on_level((ns, db).into())
} }
pub fn on_scope(self, ns: &str, db: &str, scope: &str) -> Resource { pub fn on_record(self, ns: &str, db: &str, rid: &str) -> Resource {
self.on_level((ns, db, scope).into()) self.on_level((ns, db, rid).into())
} }
} }

View file

@ -16,7 +16,7 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy<serde_json::Value> = Lazy::new(|| {
}, },
}, },
"entityTypes": { "entityTypes": {
// Represents the Root, Namespace, Database and Scope levels // Represents the Root, Namespace, Database and Record levels
"Level": { "Level": {
"shape": { "shape": {
"type": "Record", "type": "Record",
@ -24,7 +24,7 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy<serde_json::Value> = Lazy::new(|| {
"type": { "type": "String", "required": true }, "type": { "type": "String", "required": true },
"ns": { "type": "String", "required": false }, "ns": { "type": "String", "required": false },
"db": { "type": "String", "required": false }, "db": { "type": "String", "required": false },
"scope": { "type": "String", "required": false }, "rid": { "type": "String", "required": false },
"table": { "type": "String", "required": false }, "table": { "type": "String", "required": false },
"level" : { "type": "Entity", "name": "Level", "required": true }, "level" : { "type": "Entity", "name": "Level", "required": true },
} }
@ -36,7 +36,7 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy<serde_json::Value> = Lazy::new(|| {
"Any": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Any": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Namespace": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Namespace": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Database": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Database": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Scope": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Record": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Table": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Table": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Document": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Document": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Option": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Option": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
@ -65,14 +65,14 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy<serde_json::Value> = Lazy::new(|| {
"View": { "View": {
"appliesTo": { "appliesTo": {
"principalTypes": [ "Actor" ], "principalTypes": [ "Actor" ],
"resourceTypes": [ "Any", "Namespace", "Database", "Scope", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Actor" ], "resourceTypes": [ "Any", "Namespace", "Database", "Record", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Actor" ],
}, },
}, },
"Edit": { "Edit": {
"appliesTo": { "appliesTo": {
"principalTypes": [ "Actor" ], "principalTypes": [ "Actor" ],
"resourceTypes": [ "Any", "Namespace", "Database", "Scope", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Actor" ], "resourceTypes": [ "Any", "Namespace", "Database", "Record", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Actor" ],
}, },
}, },
}, },

43
core/src/iam/issue.rs Normal file
View file

@ -0,0 +1,43 @@
use crate::err::Error;
use crate::sql::duration::Duration;
use crate::sql::Algorithm;
use chrono::Duration as ChronoDuration;
use chrono::Utc;
use jsonwebtoken::EncodingKey;
pub(crate) fn config(alg: Algorithm, key: String) -> Result<EncodingKey, Error> {
match alg {
Algorithm::Hs256 => Ok(EncodingKey::from_secret(key.as_ref())),
Algorithm::Hs384 => Ok(EncodingKey::from_secret(key.as_ref())),
Algorithm::Hs512 => Ok(EncodingKey::from_secret(key.as_ref())),
Algorithm::EdDSA => Ok(EncodingKey::from_ed_pem(key.as_ref())?),
Algorithm::Es256 => Ok(EncodingKey::from_ec_pem(key.as_ref())?),
Algorithm::Es384 => Ok(EncodingKey::from_ec_pem(key.as_ref())?),
Algorithm::Es512 => Ok(EncodingKey::from_ec_pem(key.as_ref())?),
Algorithm::Ps256 => Ok(EncodingKey::from_rsa_pem(key.as_ref())?),
Algorithm::Ps384 => Ok(EncodingKey::from_rsa_pem(key.as_ref())?),
Algorithm::Ps512 => Ok(EncodingKey::from_rsa_pem(key.as_ref())?),
Algorithm::Rs256 => Ok(EncodingKey::from_rsa_pem(key.as_ref())?),
Algorithm::Rs384 => Ok(EncodingKey::from_rsa_pem(key.as_ref())?),
Algorithm::Rs512 => Ok(EncodingKey::from_rsa_pem(key.as_ref())?),
}
}
pub(crate) fn expiration(d: Option<Duration>) -> Result<Option<i64>, Error> {
let exp = match d {
Some(v) => {
// The defined duration must be valid
match ChronoDuration::from_std(v.0) {
// The resulting expiration must be valid
Ok(d) => match Utc::now().checked_add_signed(d) {
Some(exp) => Some(exp.timestamp()),
None => return Err(Error::AccessInvalidExpiration),
},
Err(_) => return Err(Error::AccessInvalidDuration),
}
}
_ => None,
};
Ok(exp)
}

View file

@ -7,6 +7,7 @@ pub mod base;
pub mod check; pub mod check;
pub mod clear; pub mod clear;
pub mod entities; pub mod entities;
pub mod issue;
#[cfg(feature = "jwks")] #[cfg(feature = "jwks")]
pub mod jwks; pub mod jwks;
pub mod policies; pub mod policies;

View file

@ -24,7 +24,7 @@ pub static POLICY_SET: Lazy<PolicySet> = Lazy::new(|| {
) when { ) when {
principal.roles.contains(Role::"Editor") && principal.roles.contains(Role::"Editor") &&
resource.level in principal.level && resource.level in principal.level &&
["Namespace", "Database", "Scope", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index"].contains(resource.type) ["Namespace", "Database", "Record", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index"].contains(resource.type)
}; };
// Owner role can edit all resources on the same level hierarchy or below // Owner role can edit all resources on the same level hierarchy or below

View file

@ -1,15 +1,17 @@
use super::verify::{verify_creds_legacy, verify_db_creds, verify_ns_creds, verify_root_creds}; use super::verify::{verify_creds_legacy, verify_db_creds, verify_ns_creds, verify_root_creds};
use super::{Actor, Level}; use super::{Actor, Level};
use crate::cnf::{INSECURE_FORWARD_SCOPE_ERRORS, SERVER_NAME}; use crate::cnf::{INSECURE_FORWARD_RECORD_ACCESS_ERRORS, SERVER_NAME};
use crate::dbs::Session; use crate::dbs::Session;
use crate::err::Error; use crate::err::Error;
use crate::iam::issue::{config, expiration};
use crate::iam::token::{Claims, HEADER}; use crate::iam::token::{Claims, HEADER};
use crate::iam::Auth; use crate::iam::Auth;
use crate::kvs::{Datastore, LockType::*, TransactionType::*}; use crate::kvs::{Datastore, LockType::*, TransactionType::*};
use crate::sql::AccessType;
use crate::sql::Object; use crate::sql::Object;
use crate::sql::Value; use crate::sql::Value;
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use jsonwebtoken::{encode, EncodingKey}; use jsonwebtoken::{encode, EncodingKey, Header};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@ -21,20 +23,20 @@ pub async fn signin(
// Parse the specified variables // Parse the specified variables
let ns = vars.get("NS").or_else(|| vars.get("ns")); let ns = vars.get("NS").or_else(|| vars.get("ns"));
let db = vars.get("DB").or_else(|| vars.get("db")); let db = vars.get("DB").or_else(|| vars.get("db"));
let sc = vars.get("SC").or_else(|| vars.get("sc")); let ac = vars.get("AC").or_else(|| vars.get("ac"));
// Check if the parameters exist // Check if the parameters exist
match (ns, db, sc) { match (ns, db, ac) {
// SCOPE signin // DB signin with access method
(Some(ns), Some(db), Some(sc)) => { (Some(ns), Some(db), Some(ac)) => {
// Process the provided values // Process the provided values
let ns = ns.to_raw_string(); let ns = ns.to_raw_string();
let db = db.to_raw_string(); let db = db.to_raw_string();
let sc = sc.to_raw_string(); let ac = ac.to_raw_string();
// Attempt to signin to specified scope // Attempt to signin using specified access method
super::signin::sc(kvs, session, ns, db, sc, vars).await super::signin::db_access(kvs, session, ns, db, ac, vars).await
} }
// DB signin // DB signin with user credentials
(Some(ns), Some(db), None) => { (Some(ns), Some(db), None) => {
// Get the provided user and pass // Get the provided user and pass
let user = vars.get("user"); let user = vars.get("user");
@ -49,12 +51,12 @@ pub async fn signin(
let user = user.to_raw_string(); let user = user.to_raw_string();
let pass = pass.to_raw_string(); let pass = pass.to_raw_string();
// Attempt to signin to database // Attempt to signin to database
super::signin::db(kvs, session, ns, db, user, pass).await super::signin::db_user(kvs, session, ns, db, user, pass).await
} }
_ => Err(Error::MissingUserOrPass), _ => Err(Error::MissingUserOrPass),
} }
} }
// NS signin // NS signin with user credentials
(Some(ns), None, None) => { (Some(ns), None, None) => {
// Get the provided user and pass // Get the provided user and pass
let user = vars.get("user"); let user = vars.get("user");
@ -68,12 +70,12 @@ pub async fn signin(
let user = user.to_raw_string(); let user = user.to_raw_string();
let pass = pass.to_raw_string(); let pass = pass.to_raw_string();
// Attempt to signin to namespace // Attempt to signin to namespace
super::signin::ns(kvs, session, ns, user, pass).await super::signin::ns_user(kvs, session, ns, user, pass).await
} }
_ => Err(Error::MissingUserOrPass), _ => Err(Error::MissingUserOrPass),
} }
} }
// KV signin // ROOT signin with user credentials
(None, None, None) => { (None, None, None) => {
// Get the provided user and pass // Get the provided user and pass
let user = vars.get("user"); let user = vars.get("user");
@ -86,7 +88,7 @@ pub async fn signin(
let user = user.to_raw_string(); let user = user.to_raw_string();
let pass = pass.to_raw_string(); let pass = pass.to_raw_string();
// Attempt to signin to root // Attempt to signin to root
super::signin::root(kvs, session, user, pass).await super::signin::root_user(kvs, session, user, pass).await
} }
_ => Err(Error::MissingUserOrPass), _ => Err(Error::MissingUserOrPass),
} }
@ -95,114 +97,112 @@ pub async fn signin(
} }
} }
pub async fn sc( pub async fn db_access(
kvs: &Datastore, kvs: &Datastore,
session: &mut Session, session: &mut Session,
ns: String, ns: String,
db: String, db: String,
sc: String, ac: String,
vars: Object, vars: Object,
) -> Result<Option<String>, Error> { ) -> Result<Option<String>, Error> {
// Create a new readonly transaction // Create a new readonly transaction
let mut tx = kvs.transaction(Read, Optimistic).await?; let mut tx = kvs.transaction(Read, Optimistic).await?;
// Fetch the specified scope from storage // Fetch the specified access method from storage
let scope = tx.get_sc(&ns, &db, &sc).await; let access = tx.get_db_access(&ns, &db, &ac).await;
// Ensure that the transaction is cancelled // Ensure that the transaction is cancelled
tx.cancel().await?; tx.cancel().await?;
// Check if the supplied Scope login exists // Check the provided access method exists
match scope { match access {
Ok(sv) => { Ok(av) => {
match sv.signin { // Check the access method type
// This scope allows signin // All access method types are supported except for JWT
Some(val) => { // The JWT access method is the one that is internal to SurrealDB
// Setup the query params // The equivalent of signing in with JWT is to authenticate it
let vars = Some(vars.0); match av.kind {
// Setup the system session for finding the signin record AccessType::Record(at) => {
let mut sess = Session::editor().with_ns(&ns).with_db(&db); // Check if the record access method supports issuing tokens
sess.ip.clone_from(&session.ip); let iss = match at.jwt.issue {
sess.or.clone_from(&session.or); Some(iss) => iss,
// Compute the value with the params _ => return Err(Error::AccessMethodMismatch),
match kvs.evaluate(val, &sess, vars).await { };
// The signin value succeeded match at.signin {
Ok(val) => match val.record() { // This record access allows signin
// There is a record returned Some(val) => {
Some(rid) => { // Setup the query params
// Create the authentication key let vars = Some(vars.0);
let key = EncodingKey::from_secret(sv.code.as_ref()); // Setup the system session for finding the signin record
// Create the authentication claim let mut sess = Session::editor().with_ns(&ns).with_db(&db);
let exp = Some( sess.ip.clone_from(&session.ip);
match sv.session { sess.or.clone_from(&session.or);
Some(v) => { // Compute the value with the params
// The defined session duration must be valid match kvs.evaluate(val, &sess, vars).await {
match Duration::from_std(v.0) { // The signin value succeeded
// The resulting session expiration must be valid Ok(val) => {
Ok(d) => match Utc::now().checked_add_signed(d) { match val.record() {
Some(exp) => exp, // There is a record returned
None => { Some(rid) => {
return Err(Error::InvalidSessionExpiration) // Create the authentication key
} let key = config(iss.alg, iss.key)?;
}, // Create the authentication claim
Err(_) => { let val = Claims {
return Err(Error::InvalidSessionDuration) iss: Some(SERVER_NAME.to_owned()),
} iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
// Token expiration is derived from issuer duration
exp: expiration(iss.duration)?,
jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()),
db: Some(db.to_owned()),
ac: Some(ac.to_owned()),
id: Some(rid.to_raw()),
..Claims::default()
};
// Log the authenticated access method info
trace!("Signing in with access method `{}`", ac);
// Create the authentication token
let enc =
encode(&Header::new(iss.alg.into()), &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.ac = Some(ac.to_owned());
session.rd = Some(Value::from(rid.to_owned()));
// Session expiration is derived from record access duration
session.exp = expiration(at.duration)?;
session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(),
Default::default(),
Level::Record(ns, db, rid.to_string()),
)));
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(Some(tk)),
_ => Err(Error::TokenMakingFailed),
} }
} }
_ => Utc::now() + Duration::hours(1), _ => Err(Error::NoRecordFound),
} }
.timestamp(),
);
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp,
jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()),
db: Some(db.to_owned()),
sc: Some(sc.to_owned()),
id: Some(rid.to_raw()),
..Claims::default()
};
// Log the authenticated scope info
trace!("Signing in to scope `{}`", sc);
// Create the authentication token
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.sc = Some(sc.to_owned());
session.sd = Some(Value::from(rid.to_owned()));
session.exp = exp;
session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(),
Default::default(),
Level::Scope(ns, db, sc),
)));
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(Some(tk)),
_ => Err(Error::TokenMakingFailed),
} }
Err(e) => match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
_ => Err(Error::AccessRecordSigninQueryFailed),
},
} }
_ => Err(Error::NoRecordFound), }
}, _ => Err(Error::AccessRecordNoSignin),
Err(e) => match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_SCOPE_ERRORS => Err(e),
_ => Err(Error::SigninQueryFailed),
},
} }
} }
_ => Err(Error::ScopeNoSignin), _ => Err(Error::AccessMethodMismatch),
} }
} }
_ => Err(Error::NoScopeFound), _ => Err(Error::AccessNotFound),
} }
} }
pub async fn db( pub async fn db_user(
kvs: &Datastore, kvs: &Datastore,
session: &mut Session, session: &mut Session,
ns: String, ns: String,
@ -258,7 +258,7 @@ pub async fn db(
} }
} }
pub async fn ns( pub async fn ns_user(
kvs: &Datastore, kvs: &Datastore,
session: &mut Session, session: &mut Session,
ns: String, ns: String,
@ -312,7 +312,7 @@ pub async fn ns(
} }
} }
pub async fn root( pub async fn root_user(
kvs: &Datastore, kvs: &Datastore,
session: &mut Session, session: &mut Session,
user: String, user: String,
@ -370,14 +370,14 @@ mod tests {
use std::collections::HashMap; use std::collections::HashMap;
#[tokio::test] #[tokio::test]
async fn test_signin_scope() { async fn test_signin_record() {
// Test with correct credentials // Test with correct credentials
{ {
let ds = Datastore::new("memory").await.unwrap(); let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
r#" r#"
DEFINE SCOPE user SESSION 1h DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h
SIGNIN ( SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass) SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
) )
@ -408,7 +408,7 @@ mod tests {
let mut vars: HashMap<&str, Value> = HashMap::new(); let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("user", "user".into()); vars.insert("user", "user".into());
vars.insert("pass", "pass".into()); vars.insert("pass", "pass".into());
let res = sc( let res = db_access(
&ds, &ds,
&mut sess, &mut sess,
"test".to_string(), "test".to_string(),
@ -422,10 +422,11 @@ mod tests {
assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.au.id(), "user:test"); assert_eq!(sess.au.id(), "user:test");
assert!(sess.au.is_scope()); assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test")); assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test")); assert_eq!(sess.au.level().db(), Some("test"));
// Scope users should not have roles assert_eq!(sess.au.level().id(), Some("user:test"));
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role"); assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
@ -436,7 +437,7 @@ mod tests {
let max_exp = (Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp(); let max_exp = (Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp();
assert!( assert!(
exp > min_exp && exp < max_exp, exp > min_exp && exp < max_exp,
"Session expiration is expected to follow scope duration" "Session expiration is expected to follow access method duration"
); );
} }
@ -446,7 +447,7 @@ mod tests {
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
r#" r#"
DEFINE SCOPE user SESSION 1h DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h
SIGNIN ( SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass) SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
) )
@ -477,7 +478,7 @@ mod tests {
let mut vars: HashMap<&str, Value> = HashMap::new(); let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("user", "user".into()); vars.insert("user", "user".into());
vars.insert("pass", "incorrect".into()); vars.insert("pass", "incorrect".into());
let res = sc( let res = db_access(
&ds, &ds,
&mut sess, &mut sess,
"test".to_string(), "test".to_string(),
@ -492,7 +493,161 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn test_signin_db() { async fn test_signin_record_with_jwt_issuer() {
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
// Test with correct credentials
{
let public_key = r#"-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----"#;
let private_key = r#"-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
dn/RsYEONbwQSjIfMPkvxF+8HQ==
-----END PRIVATE KEY-----"#;
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute(
&format!(
r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD
DURATION 1h
SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
)
SIGNUP (
CREATE user CONTENT {{
name: $user,
pass: crypto::argon2::generate($pass)
}}
)
WITH JWT ALGORITHM RS256 KEY '{public_key}'
WITH ISSUER KEY '{private_key}' DURATION 15m
;
CREATE user:test CONTENT {{
name: 'user',
pass: crypto::argon2::generate('pass')
}}
"#
),
&sess,
None,
)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("user", "user".into());
vars.insert("pass", "pass".into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
vars.into(),
)
.await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.au.id(), "user:test");
assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
assert_eq!(sess.au.level().id(), Some("user:test"));
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Session expiration should always be set for tokens issued by SurrealDB
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_sess_exp =
(Utc::now() + Duration::hours(1) - Duration::seconds(10)).timestamp();
let max_sess_exp =
(Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp();
assert!(
exp > min_sess_exp && exp < max_sess_exp,
"Session expiration is expected to follow access method duration"
);
// Decode token and check that it has been issued as intended
if let Ok(Some(tk)) = res {
// Check that token can be verified with the defined algorithm
let val = Validation::new(Algorithm::RS256);
// Check that token can be verified with the defined public key
let token_data = decode::<Claims>(
&tk,
&DecodingKey::from_rsa_pem(public_key.as_ref()).unwrap(),
&val,
)
.unwrap();
// Check that token has been issued with the defined algorithm
assert_eq!(token_data.header.alg, Algorithm::RS256);
// Check that token expiration matches the defined duration
// Expiration should match the current time plus token duration with some margin
let exp = match token_data.claims.exp {
Some(exp) => exp,
_ => panic!("Token is missing expiration claim"),
};
let min_tk_exp =
(Utc::now() + Duration::minutes(15) - Duration::seconds(10)).timestamp();
let max_tk_exp =
(Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
assert!(
exp > min_tk_exp && exp < max_tk_exp,
"Token expiration is expected to follow issuer duration"
);
// Check required token claims
assert_eq!(token_data.claims.ns, Some("test".to_string()));
assert_eq!(token_data.claims.db, Some("test".to_string()));
assert_eq!(token_data.claims.id, Some("user:test".to_string()));
assert_eq!(token_data.claims.ac, Some("user".to_string()));
} else {
panic!("Token could not be extracted from result")
}
}
}
#[tokio::test]
async fn test_signin_db_user() {
// //
// Test without roles defined // Test without roles defined
// //
@ -507,7 +662,7 @@ mod tests {
db: Some("test".to_string()), db: Some("test".to_string()),
..Default::default() ..Default::default()
}; };
let res = db( let res = db_user(
&ds, &ds,
&mut sess, &mut sess,
"test".to_string(), "test".to_string(),
@ -546,7 +701,7 @@ mod tests {
db: Some("test".to_string()), db: Some("test".to_string()),
..Default::default() ..Default::default()
}; };
let res = db( let res = db_user(
&ds, &ds,
&mut sess, &mut sess,
"test".to_string(), "test".to_string(),
@ -578,7 +733,7 @@ mod tests {
let mut sess = Session { let mut sess = Session {
..Default::default() ..Default::default()
}; };
let res = db( let res = db_user(
&ds, &ds,
&mut sess, &mut sess,
"test".to_string(), "test".to_string(),
@ -593,7 +748,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn test_signin_ns() { async fn test_signin_ns_user() {
// //
// Test without roles defined // Test without roles defined
// //
@ -608,7 +763,7 @@ mod tests {
..Default::default() ..Default::default()
}; };
let res = let res =
ns(&ds, &mut sess, "test".to_string(), "user".to_string(), "pass".to_string()) ns_user(&ds, &mut sess, "test".to_string(), "user".to_string(), "pass".to_string())
.await; .await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res); assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
@ -638,7 +793,7 @@ mod tests {
..Default::default() ..Default::default()
}; };
let res = let res =
ns(&ds, &mut sess, "test".to_string(), "user".to_string(), "pass".to_string()) ns_user(&ds, &mut sess, "test".to_string(), "user".to_string(), "pass".to_string())
.await; .await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res); assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
@ -661,16 +816,21 @@ mod tests {
let mut sess = Session { let mut sess = Session {
..Default::default() ..Default::default()
}; };
let res = let res = ns_user(
ns(&ds, &mut sess, "test".to_string(), "user".to_string(), "invalid".to_string()) &ds,
.await; &mut sess,
"test".to_string(),
"user".to_string(),
"invalid".to_string(),
)
.await;
assert!(res.is_err(), "Unexpected successful signin: {:?}", res); assert!(res.is_err(), "Unexpected successful signin: {:?}", res);
} }
} }
#[tokio::test] #[tokio::test]
async fn test_signin_root() { async fn test_signin_root_user() {
// //
// Test without roles defined // Test without roles defined
// //
@ -683,7 +843,7 @@ mod tests {
let mut sess = Session { let mut sess = Session {
..Default::default() ..Default::default()
}; };
let res = root(&ds, &mut sess, "user".to_string(), "pass".to_string()).await; let res = root_user(&ds, &mut sess, "user".to_string(), "pass".to_string()).await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res); assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.au.id(), "user"); assert_eq!(sess.au.id(), "user");
@ -708,7 +868,7 @@ mod tests {
let mut sess = Session { let mut sess = Session {
..Default::default() ..Default::default()
}; };
let res = root(&ds, &mut sess, "user".to_string(), "pass".to_string()).await; let res = root_user(&ds, &mut sess, "user".to_string(), "pass".to_string()).await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res); assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.au.id(), "user"); assert_eq!(sess.au.id(), "user");
@ -728,7 +888,7 @@ mod tests {
let mut sess = Session { let mut sess = Session {
..Default::default() ..Default::default()
}; };
let res = root(&ds, &mut sess, "user".to_string(), "invalid".to_string()).await; let res = root_user(&ds, &mut sess, "user".to_string(), "invalid".to_string()).await;
assert!(res.is_err(), "Unexpected successful signin: {:?}", res); assert!(res.is_err(), "Unexpected successful signin: {:?}", res);
} }

View file

@ -1,14 +1,16 @@
use crate::cnf::{INSECURE_FORWARD_SCOPE_ERRORS, SERVER_NAME}; use crate::cnf::{INSECURE_FORWARD_RECORD_ACCESS_ERRORS, SERVER_NAME};
use crate::dbs::Session; use crate::dbs::Session;
use crate::err::Error; use crate::err::Error;
use crate::iam::token::{Claims, HEADER}; use crate::iam::issue::{config, expiration};
use crate::iam::token::Claims;
use crate::iam::Auth; use crate::iam::Auth;
use crate::iam::{Actor, Level}; use crate::iam::{Actor, Level};
use crate::kvs::{Datastore, LockType::*, TransactionType::*}; use crate::kvs::{Datastore, LockType::*, TransactionType::*};
use crate::sql::AccessType;
use crate::sql::Object; use crate::sql::Object;
use crate::sql::Value; use crate::sql::Value;
use chrono::{Duration, Utc}; use chrono::Utc;
use jsonwebtoken::{encode, EncodingKey}; use jsonwebtoken::{encode, Header};
use std::sync::Arc; use std::sync::Arc;
use uuid::Uuid; use uuid::Uuid;
@ -20,125 +22,122 @@ pub async fn signup(
// Parse the specified variables // Parse the specified variables
let ns = vars.get("NS").or_else(|| vars.get("ns")); let ns = vars.get("NS").or_else(|| vars.get("ns"));
let db = vars.get("DB").or_else(|| vars.get("db")); let db = vars.get("DB").or_else(|| vars.get("db"));
let sc = vars.get("SC").or_else(|| vars.get("sc")); let ac = vars.get("AC").or_else(|| vars.get("ac"));
// Check if the parameters exist // Check if the parameters exist
match (ns, db, sc) { match (ns, db, ac) {
(Some(ns), Some(db), Some(sc)) => { (Some(ns), Some(db), Some(ac)) => {
// Process the provided values // Process the provided values
let ns = ns.to_raw_string(); let ns = ns.to_raw_string();
let db = db.to_raw_string(); let db = db.to_raw_string();
let sc = sc.to_raw_string(); let ac = ac.to_raw_string();
// Attempt to signup to specified scope // Attempt to signup using specified access method
super::signup::sc(kvs, session, ns, db, sc, vars).await // Currently, signup is only supported at the database level
super::signup::db_access(kvs, session, ns, db, ac, vars).await
} }
_ => Err(Error::InvalidSignup), _ => Err(Error::InvalidSignup),
} }
} }
pub async fn sc( pub async fn db_access(
kvs: &Datastore, kvs: &Datastore,
session: &mut Session, session: &mut Session,
ns: String, ns: String,
db: String, db: String,
sc: String, ac: String,
vars: Object, vars: Object,
) -> Result<Option<String>, Error> { ) -> Result<Option<String>, Error> {
// Create a new readonly transaction // Create a new readonly transaction
let mut tx = kvs.transaction(Read, Optimistic).await?; let mut tx = kvs.transaction(Read, Optimistic).await?;
// Fetch the specified scope from storage // Fetch the specified access method from storage
let scope = tx.get_sc(&ns, &db, &sc).await; let access = tx.get_db_access(&ns, &db, &ac).await;
// Ensure that the transaction is cancelled // Ensure that the transaction is cancelled
tx.cancel().await?; tx.cancel().await?;
// Check if the supplied Scope login exists // Check the provided access method exists
match scope { match access {
Ok(sv) => { Ok(av) => {
match sv.signup { // Check the access method type
// This scope allows signup // Currently, only the record access method supports signup
Some(val) => { match av.kind {
// Setup the query params AccessType::Record(at) => {
let vars = Some(vars.0); // Check if the record access method supports issuing tokens
// Setup the system session for creating the signup record let iss = match at.jwt.issue {
let mut sess = Session::editor().with_ns(&ns).with_db(&db); Some(iss) => iss,
sess.ip.clone_from(&session.ip); _ => return Err(Error::AccessMethodMismatch),
sess.or.clone_from(&session.or); };
// Compute the value with the params match at.signup {
match kvs.evaluate(val, &sess, vars).await { // This record access allows signup
// The signin value succeeded Some(val) => {
Ok(val) => match val.record() { // Setup the query params
// There is a record returned let vars = Some(vars.0);
Some(rid) => { // Setup the system session for finding the signup record
// Create the authentication key let mut sess = Session::editor().with_ns(&ns).with_db(&db);
let key = EncodingKey::from_secret(sv.code.as_ref()); sess.ip.clone_from(&session.ip);
// Create the authentication claim sess.or.clone_from(&session.or);
let exp = Some( // Compute the value with the params
match sv.session { match kvs.evaluate(val, &sess, vars).await {
Some(v) => { // The signin value succeeded
// The defined session duration must be valid Ok(val) => {
match Duration::from_std(v.0) { match val.record() {
// The resulting session expiration must be valid // There is a record returned
Ok(d) => match Utc::now().checked_add_signed(d) { Some(rid) => {
Some(exp) => exp, // Create the authentication key
None => { let key = config(iss.alg, iss.key)?;
return Err(Error::InvalidSessionExpiration) // Create the authentication claim
} let val = Claims {
}, iss: Some(SERVER_NAME.to_owned()),
Err(_) => { iat: Some(Utc::now().timestamp()),
return Err(Error::InvalidSessionDuration) nbf: Some(Utc::now().timestamp()),
} // Token expiration is derived from issuer duration
exp: expiration(iss.duration)?,
jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()),
db: Some(db.to_owned()),
ac: Some(ac.to_owned()),
id: Some(rid.to_raw()),
..Claims::default()
};
// Log the authenticated access method info
trace!("Signing up with access method `{}`", ac);
// Create the authentication token
let enc =
encode(&Header::new(iss.alg.into()), &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.ac = Some(ac.to_owned());
session.rd = Some(Value::from(rid.to_owned()));
// Session expiration is derived from record access duration
session.exp = expiration(at.duration)?;
session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(),
Default::default(),
Level::Record(ns, db, rid.to_string()),
)));
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(Some(tk)),
_ => Err(Error::TokenMakingFailed),
} }
} }
_ => Utc::now() + Duration::hours(1), _ => Err(Error::NoRecordFound),
} }
.timestamp(),
);
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
jti: Some(Uuid::new_v4().to_string()),
exp,
ns: Some(ns.to_owned()),
db: Some(db.to_owned()),
sc: Some(sc.to_owned()),
id: Some(rid.to_raw()),
..Claims::default()
};
// Log the authenticated scope info
trace!("Signing up to scope `{}`", sc);
// Create the authentication token
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.sc = Some(sc.to_owned());
session.sd = Some(Value::from(rid.to_owned()));
session.exp = exp;
session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(),
Default::default(),
Level::Scope(ns, db, sc),
)));
// Create the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(Some(tk)),
_ => Err(Error::TokenMakingFailed),
} }
Err(e) => match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
_ => Err(Error::AccessRecordSignupQueryFailed),
},
} }
_ => Err(Error::NoRecordFound), }
}, _ => Err(Error::AccessRecordNoSignup),
Err(e) => match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_SCOPE_ERRORS => Err(e),
_ => Err(Error::SignupQueryFailed),
},
} }
} }
_ => Err(Error::ScopeNoSignup), _ => Err(Error::AccessMethodMismatch),
} }
} }
_ => Err(Error::NoScopeFound), _ => Err(Error::AccessNotFound),
} }
} }
@ -146,17 +145,18 @@ pub async fn sc(
mod tests { mod tests {
use super::*; use super::*;
use crate::iam::Role; use crate::iam::Role;
use chrono::Duration;
use std::collections::HashMap; use std::collections::HashMap;
#[tokio::test] #[tokio::test]
async fn test_scope_signup() { async fn test_record_signup() {
// Test with valid parameters // Test with valid parameters
{ {
let ds = Datastore::new("memory").await.unwrap(); let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
r#" r#"
DEFINE SCOPE user SESSION 1h DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h
SIGNIN ( SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass) SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
) )
@ -182,7 +182,7 @@ mod tests {
let mut vars: HashMap<&str, Value> = HashMap::new(); let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("user", "user".into()); vars.insert("user", "user".into());
vars.insert("pass", "pass".into()); vars.insert("pass", "pass".into());
let res = sc( let res = db_access(
&ds, &ds,
&mut sess, &mut sess,
"test".to_string(), "test".to_string(),
@ -196,10 +196,10 @@ mod tests {
assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string()));
assert!(sess.au.id().starts_with("user:")); assert!(sess.au.id().starts_with("user:"));
assert!(sess.au.is_scope()); assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test")); assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test")); assert_eq!(sess.au.level().db(), Some("test"));
// Scope users should not have roles. // Record users should not have roles.
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role"); assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role"); assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
@ -210,7 +210,7 @@ mod tests {
let max_exp = (Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp(); let max_exp = (Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp();
assert!( assert!(
exp > min_exp && exp < max_exp, exp > min_exp && exp < max_exp,
"Session expiration is expected to follow scope duration" "Session expiration is expected to follow token duration"
); );
} }
@ -220,7 +220,7 @@ mod tests {
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
r#" r#"
DEFINE SCOPE user SESSION 1h DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h
SIGNIN ( SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass) SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
) )
@ -246,7 +246,7 @@ mod tests {
let mut vars: HashMap<&str, Value> = HashMap::new(); let mut vars: HashMap<&str, Value> = HashMap::new();
// Password is missing // Password is missing
vars.insert("user", "user".into()); vars.insert("user", "user".into());
let res = sc( let res = db_access(
&ds, &ds,
&mut sess, &mut sess,
"test".to_string(), "test".to_string(),
@ -259,4 +259,158 @@ mod tests {
assert!(res.is_err(), "Unexpected successful signup: {:?}", res); assert!(res.is_err(), "Unexpected successful signup: {:?}", res);
} }
} }
#[tokio::test]
async fn test_record_signup_with_jwt_issuer() {
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
// Test with valid parameters
{
let public_key = r#"-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----"#;
let private_key = r#"-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
dn/RsYEONbwQSjIfMPkvxF+8HQ==
-----END PRIVATE KEY-----"#;
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute(
&format!(
r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD
DURATION 1h
SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
)
SIGNUP (
CREATE user CONTENT {{
name: $user,
pass: crypto::argon2::generate($pass)
}}
)
WITH JWT ALGORITHM RS256 KEY '{public_key}'
WITH ISSUER KEY '{private_key}' DURATION 15m
;
CREATE user:test CONTENT {{
name: 'user',
pass: crypto::argon2::generate('pass')
}}
"#
),
&sess,
None,
)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("user", "user".into());
vars.insert("pass", "pass".into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
vars.into(),
)
.await;
assert!(res.is_ok(), "Failed to signup: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert!(sess.au.id().starts_with("user:"));
assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
// Record users should not have roles.
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Session expiration should always be set for tokens issued by SurrealDB
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_sess_exp =
(Utc::now() + Duration::hours(1) - Duration::seconds(10)).timestamp();
let max_sess_exp =
(Utc::now() + Duration::hours(1) + Duration::seconds(10)).timestamp();
assert!(
exp > min_sess_exp && exp < max_sess_exp,
"Session expiration is expected to follow access method duration"
);
// Decode token and check that it has been issued as intended
if let Ok(Some(tk)) = res {
// Check that token can be verified with the defined algorithm
let val = Validation::new(Algorithm::RS256);
// Check that token can be verified with the defined public key
let token_data = decode::<Claims>(
&tk,
&DecodingKey::from_rsa_pem(public_key.as_ref()).unwrap(),
&val,
)
.unwrap();
// Check that token has been issued with the defined algorithm
assert_eq!(token_data.header.alg, Algorithm::RS256);
// Check that token expiration matches the defined duration
// Expiration should match the current time plus token duration with some margin
let exp = match token_data.claims.exp {
Some(exp) => exp,
_ => panic!("Token is missing expiration claim"),
};
let min_tk_exp =
(Utc::now() + Duration::minutes(15) - Duration::seconds(10)).timestamp();
let max_tk_exp =
(Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
assert!(
exp > min_tk_exp && exp < max_tk_exp,
"Token expiration is expected to follow issuer duration"
);
// Check required token claims
assert_eq!(token_data.claims.ns, Some("test".to_string()));
assert_eq!(token_data.claims.db, Some("test".to_string()));
assert!(token_data.claims.id.unwrap().starts_with("user:"));
assert_eq!(token_data.claims.ac, Some("user".to_string()));
} else {
panic!("Token could not be extracted from result")
}
}
}
} }

View file

@ -35,20 +35,13 @@ pub struct Claims {
#[serde(alias = "https://surrealdb.com/database")] #[serde(alias = "https://surrealdb.com/database")]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub db: Option<String>, pub db: Option<String>,
#[serde(alias = "sc")] #[serde(alias = "ac")]
#[serde(alias = "SC")] #[serde(alias = "AC")]
#[serde(rename = "SC")] #[serde(rename = "AC")]
#[serde(alias = "https://surrealdb.com/sc")] #[serde(alias = "https://surrealdb.com/ac")]
#[serde(alias = "https://surrealdb.com/scope")] #[serde(alias = "https://surrealdb.com/access")]
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub sc: Option<String>, pub ac: Option<String>,
#[serde(alias = "tk")]
#[serde(alias = "TK")]
#[serde(rename = "TK")]
#[serde(alias = "https://surrealdb.com/tk")]
#[serde(alias = "https://surrealdb.com/token")]
#[serde(skip_serializing_if = "Option::is_none")]
pub tk: Option<String>,
#[serde(alias = "id")] #[serde(alias = "id")]
#[serde(alias = "ID")] #[serde(alias = "ID")]
#[serde(rename = "ID")] #[serde(rename = "ID")]
@ -101,13 +94,9 @@ impl From<Claims> for Value {
if let Some(db) = v.db { if let Some(db) = v.db {
out.insert("DB".to_string(), db.into()); out.insert("DB".to_string(), db.into());
} }
// Add SC field if set // Add AC field if set
if let Some(sc) = v.sc { if let Some(ac) = v.ac {
out.insert("SC".to_string(), sc.into()); out.insert("AC".to_string(), ac.into());
}
// Add TK field if set
if let Some(tk) = v.tk {
out.insert("TK".to_string(), tk.into());
} }
// Add ID field if set // Add ID field if set
if let Some(id) = v.id { if let Some(id) = v.id {

View file

@ -4,94 +4,70 @@ use crate::err::Error;
use crate::iam::jwks; use crate::iam::jwks;
use crate::iam::{token::Claims, Actor, Auth, Level, Role}; use crate::iam::{token::Claims, Actor, Auth, Level, Role};
use crate::kvs::{Datastore, LockType::*, TransactionType::*}; use crate::kvs::{Datastore, LockType::*, TransactionType::*};
use crate::sql::access_type::{AccessType, JwtAccessVerify};
use crate::sql::{statements::DefineUserStatement, Algorithm, Value}; use crate::sql::{statements::DefineUserStatement, Algorithm, Value};
use crate::syn; use crate::syn;
use argon2::{Argon2, PasswordHash, PasswordVerifier}; use argon2::{Argon2, PasswordHash, PasswordVerifier};
use chrono::Utc; use chrono::Utc;
use jsonwebtoken::{decode, DecodingKey, Header, Validation}; use jsonwebtoken::{decode, DecodingKey, Validation};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::str::{self, FromStr}; use std::str::{self, FromStr};
use std::sync::Arc; use std::sync::Arc;
async fn config( fn config(alg: Algorithm, key: String) -> Result<(DecodingKey, Validation), Error> {
_kvs: &Datastore, match alg {
de_kind: Algorithm,
de_code: String,
_token_header: Header,
) -> Result<(DecodingKey, Validation), Error> {
if de_kind == Algorithm::Jwks {
#[cfg(not(feature = "jwks"))]
{
warn!("Failed to verify a token defined as JWKS when the feature is not enabled");
Err(Error::InvalidAuth)
}
#[cfg(feature = "jwks")]
// The key identifier header must be present
if let Some(kid) = _token_header.kid {
jwks::config(_kvs, &kid, &de_code, _token_header.alg).await
} else {
Err(Error::MissingTokenHeader("kid".to_string()))
}
} else {
config_alg(de_kind, de_code)
}
}
fn config_alg(algo: Algorithm, code: String) -> Result<(DecodingKey, Validation), Error> {
match algo {
Algorithm::Hs256 => Ok(( Algorithm::Hs256 => Ok((
DecodingKey::from_secret(code.as_ref()), DecodingKey::from_secret(key.as_ref()),
Validation::new(jsonwebtoken::Algorithm::HS256), Validation::new(jsonwebtoken::Algorithm::HS256),
)), )),
Algorithm::Hs384 => Ok(( Algorithm::Hs384 => Ok((
DecodingKey::from_secret(code.as_ref()), DecodingKey::from_secret(key.as_ref()),
Validation::new(jsonwebtoken::Algorithm::HS384), Validation::new(jsonwebtoken::Algorithm::HS384),
)), )),
Algorithm::Hs512 => Ok(( Algorithm::Hs512 => Ok((
DecodingKey::from_secret(code.as_ref()), DecodingKey::from_secret(key.as_ref()),
Validation::new(jsonwebtoken::Algorithm::HS512), Validation::new(jsonwebtoken::Algorithm::HS512),
)), )),
Algorithm::EdDSA => Ok(( Algorithm::EdDSA => Ok((
DecodingKey::from_ed_pem(code.as_ref())?, DecodingKey::from_ed_pem(key.as_ref())?,
Validation::new(jsonwebtoken::Algorithm::EdDSA), Validation::new(jsonwebtoken::Algorithm::EdDSA),
)), )),
Algorithm::Es256 => Ok(( Algorithm::Es256 => Ok((
DecodingKey::from_ec_pem(code.as_ref())?, DecodingKey::from_ec_pem(key.as_ref())?,
Validation::new(jsonwebtoken::Algorithm::ES256), Validation::new(jsonwebtoken::Algorithm::ES256),
)), )),
Algorithm::Es384 => Ok(( Algorithm::Es384 => Ok((
DecodingKey::from_ec_pem(code.as_ref())?, DecodingKey::from_ec_pem(key.as_ref())?,
Validation::new(jsonwebtoken::Algorithm::ES384), Validation::new(jsonwebtoken::Algorithm::ES384),
)), )),
Algorithm::Es512 => Ok(( Algorithm::Es512 => Ok((
DecodingKey::from_ec_pem(code.as_ref())?, DecodingKey::from_ec_pem(key.as_ref())?,
Validation::new(jsonwebtoken::Algorithm::ES384), Validation::new(jsonwebtoken::Algorithm::ES384),
)), )),
Algorithm::Ps256 => Ok(( Algorithm::Ps256 => Ok((
DecodingKey::from_rsa_pem(code.as_ref())?, DecodingKey::from_rsa_pem(key.as_ref())?,
Validation::new(jsonwebtoken::Algorithm::PS256), Validation::new(jsonwebtoken::Algorithm::PS256),
)), )),
Algorithm::Ps384 => Ok(( Algorithm::Ps384 => Ok((
DecodingKey::from_rsa_pem(code.as_ref())?, DecodingKey::from_rsa_pem(key.as_ref())?,
Validation::new(jsonwebtoken::Algorithm::PS384), Validation::new(jsonwebtoken::Algorithm::PS384),
)), )),
Algorithm::Ps512 => Ok(( Algorithm::Ps512 => Ok((
DecodingKey::from_rsa_pem(code.as_ref())?, DecodingKey::from_rsa_pem(key.as_ref())?,
Validation::new(jsonwebtoken::Algorithm::PS512), Validation::new(jsonwebtoken::Algorithm::PS512),
)), )),
Algorithm::Rs256 => Ok(( Algorithm::Rs256 => Ok((
DecodingKey::from_rsa_pem(code.as_ref())?, DecodingKey::from_rsa_pem(key.as_ref())?,
Validation::new(jsonwebtoken::Algorithm::RS256), Validation::new(jsonwebtoken::Algorithm::RS256),
)), )),
Algorithm::Rs384 => Ok(( Algorithm::Rs384 => Ok((
DecodingKey::from_rsa_pem(code.as_ref())?, DecodingKey::from_rsa_pem(key.as_ref())?,
Validation::new(jsonwebtoken::Algorithm::RS384), Validation::new(jsonwebtoken::Algorithm::RS384),
)), )),
Algorithm::Rs512 => Ok(( Algorithm::Rs512 => Ok((
DecodingKey::from_rsa_pem(code.as_ref())?, DecodingKey::from_rsa_pem(key.as_ref())?,
Validation::new(jsonwebtoken::Algorithm::RS512), Validation::new(jsonwebtoken::Algorithm::RS512),
)), )),
Algorithm::Jwks => Err(Error::InvalidAuth), // We should never get here
} }
} }
@ -215,96 +191,87 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
} }
// Check the token authentication claims // Check the token authentication claims
match token_data.claims { match token_data.claims {
// Check if this is scope token authentication // Check if this is record access
Claims { Claims {
ns: Some(ns), ns: Some(ns),
db: Some(db), db: Some(db),
sc: Some(sc), ac: Some(ac),
tk: Some(tk),
id,
..
} => {
// Log the decoded authentication claims
trace!("Authenticating to scope `{}` with token `{}`", sc, tk);
// Create a new readonly transaction
let mut tx = kvs.transaction(Read, Optimistic).await?;
// Parse the record id
let id = match id {
Some(id) => syn::thing(&id)?.into(),
None => Value::None,
};
// Get the scope token
let de = tx.get_sc_token(&ns, &db, &sc, &tk).await?;
// Obtain the configuration with which to verify the token
let cf = config(kvs, de.kind, de.code, token_data.header).await?;
// Verify the token
decode::<Claims>(token, &cf.0, &cf.1)?;
// Log the success
debug!("Authenticated to scope `{}` with token `{}`", sc, tk);
// Set the session
session.sd = Some(id);
session.tk = Some(value);
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.sc = Some(sc.to_owned());
session.exp = token_data.claims.exp;
session.au = Arc::new(Auth::new(Actor::new(
de.name.to_string(),
Default::default(),
Level::Scope(ns, db, sc),
)));
Ok(())
}
// Check if this is scope authentication
Claims {
ns: Some(ns),
db: Some(db),
sc: Some(sc),
id: Some(id), id: Some(id),
.. ..
} => { } => {
// Log the decoded authentication claims // Log the decoded authentication claims
trace!("Authenticating to scope `{}`", sc); trace!("Authenticating with record access method `{}`", ac);
// Create a new readonly transaction // Create a new readonly transaction
let mut tx = kvs.transaction(Read, Optimistic).await?; let mut tx = kvs.transaction(Read, Optimistic).await?;
// Parse the record id // Parse the record id
let id = syn::thing(&id)?; let id = syn::thing(&id)?;
// Get the scope // Get the database access method
let de = tx.get_sc(&ns, &db, &sc).await?; let de = tx.get_db_access(&ns, &db, &ac).await?;
let cf = config_alg(Algorithm::Hs512, de.code)?; // Obtain the configuration to verify the token based on the access method
let cf = match de.kind {
AccessType::Record(ac) => match ac.jwt.verify {
JwtAccessVerify::Key(key) => config(key.alg, key.key),
#[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => {
if let Some(kid) = token_data.header.kid {
jwks::config(kvs, &kid, &jwks.url, token_data.header.alg).await
} else {
Err(Error::MissingTokenHeader("kid".to_string()))
}
}
#[cfg(not(feature = "jwks"))]
_ => return Err(Error::AccessMethodMismatch),
},
_ => return Err(Error::AccessMethodMismatch),
}?;
// Verify the token // Verify the token
decode::<Claims>(token, &cf.0, &cf.1)?; decode::<Claims>(token, &cf.0, &cf.1)?;
// Log the success // Log the success
debug!("Authenticated to scope `{}`", sc); debug!("Authenticated with record access method `{}`", ac);
// Set the session // Set the session
session.tk = Some(value); session.tk = Some(value);
session.ns = Some(ns.to_owned()); session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned()); session.db = Some(db.to_owned());
session.sc = Some(sc.to_owned()); session.ac = Some(ac.to_owned());
session.sd = Some(Value::from(id.to_owned())); session.rd = Some(Value::from(id.to_owned()));
session.exp = token_data.claims.exp; session.exp = token_data.claims.exp;
session.au = Arc::new(Auth::new(Actor::new( session.au = Arc::new(Auth::new(Actor::new(
id.to_string(), id.to_string(),
Default::default(), Default::default(),
Level::Scope(ns, db, sc), Level::Record(ns, db, id.to_string()),
))); )));
Ok(()) Ok(())
} }
// Check if this is database token authentication // Check if this is database access
Claims { Claims {
ns: Some(ns), ns: Some(ns),
db: Some(db), db: Some(db),
tk: Some(tk), ac: Some(ac),
.. ..
} => { } => {
// Log the decoded authentication claims // Log the decoded authentication claims
trace!("Authenticating to database `{}` with token `{}`", db, tk); trace!("Authenticating to database `{}` with access method `{}`", db, ac);
// Create a new readonly transaction // Create a new readonly transaction
let mut tx = kvs.transaction(Read, Optimistic).await?; let mut tx = kvs.transaction(Read, Optimistic).await?;
// Get the database token // Get the database access method
let de = tx.get_db_token(&ns, &db, &tk).await?; let de = tx.get_db_access(&ns, &db, &ac).await?;
// Obtain the configuration with which to verify the token // Obtain the configuration to verify the token based on the access method
let cf = config(kvs, de.kind, de.code, token_data.header).await?; let cf = match de.kind {
AccessType::Jwt(ac) => match ac.verify {
JwtAccessVerify::Key(key) => config(key.alg, key.key),
#[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => {
if let Some(kid) = token_data.header.kid {
jwks::config(kvs, &kid, &jwks.url, token_data.header.alg).await
} else {
Err(Error::MissingTokenHeader("kid".to_string()))
}
}
#[cfg(not(feature = "jwks"))]
_ => return Err(Error::AccessMethodMismatch),
},
_ => return Err(Error::AccessMethodMismatch),
}?;
// Verify the token // Verify the token
decode::<Claims>(token, &cf.0, &cf.1)?; decode::<Claims>(token, &cf.0, &cf.1)?;
// Parse the roles // Parse the roles
@ -320,11 +287,12 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()?,
}; };
// Log the success // Log the success
debug!("Authenticated to database `{}` with token `{}`", db, tk); debug!("Authenticated to database `{}` with access method `{}`", db, ac);
// Set the session // Set the session
session.tk = Some(value); session.tk = Some(value);
session.ns = Some(ns.to_owned()); session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned()); session.db = Some(db.to_owned());
session.ac = Some(ac.to_owned());
session.exp = token_data.claims.exp; session.exp = token_data.claims.exp;
session.au = Arc::new(Auth::new(Actor::new( session.au = Arc::new(Auth::new(Actor::new(
de.name.to_string(), de.name.to_string(),
@ -333,7 +301,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
))); )));
Ok(()) Ok(())
} }
// Check if this is database authentication // Check if this is database authentication with user credentials
Claims { Claims {
ns: Some(ns), ns: Some(ns),
db: Some(db), db: Some(db),
@ -349,7 +317,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
trace!("Error while authenticating to database `{db}`: {e}"); trace!("Error while authenticating to database `{db}`: {e}");
Error::InvalidAuth Error::InvalidAuth
})?; })?;
let cf = config_alg(Algorithm::Hs512, de.code)?; let cf = config(Algorithm::Hs512, de.code)?;
// Verify the token // Verify the token
decode::<Claims>(token, &cf.0, &cf.1)?; decode::<Claims>(token, &cf.0, &cf.1)?;
// Log the success // Log the success
@ -366,20 +334,35 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
))); )));
Ok(()) Ok(())
} }
// Check if this is namespace token authentication // Check if this is namespace access
Claims { Claims {
ns: Some(ns), ns: Some(ns),
tk: Some(tk), ac: Some(ac),
.. ..
} => { } => {
// Log the decoded authentication claims // Log the decoded authentication claims
trace!("Authenticating to namespace `{}` with token `{}`", ns, tk); trace!("Authenticating to namespace `{}` with access method `{}`", ns, ac);
// Create a new readonly transaction // Create a new readonly transaction
let mut tx = kvs.transaction(Read, Optimistic).await?; let mut tx = kvs.transaction(Read, Optimistic).await?;
// Get the namespace token // Get the namespace access method
let de = tx.get_ns_token(&ns, &tk).await?; let de = tx.get_ns_access(&ns, &ac).await?;
// Obtain the configuration with which to verify the token // Obtain the configuration to verify the token based on the access method
let cf = config(kvs, de.kind, de.code, token_data.header).await?; let cf = match de.kind {
AccessType::Jwt(ac) => match ac.verify {
JwtAccessVerify::Key(key) => config(key.alg, key.key),
#[cfg(feature = "jwks")]
JwtAccessVerify::Jwks(jwks) => {
if let Some(kid) = token_data.header.kid {
jwks::config(kvs, &kid, &jwks.url, token_data.header.alg).await
} else {
Err(Error::MissingTokenHeader("kid".to_string()))
}
}
#[cfg(not(feature = "jwks"))]
_ => return Err(Error::AccessMethodMismatch),
},
_ => return Err(Error::AccessMethodMismatch),
}?;
// Verify the token // Verify the token
decode::<Claims>(token, &cf.0, &cf.1)?; decode::<Claims>(token, &cf.0, &cf.1)?;
// Parse the roles // Parse the roles
@ -395,16 +378,17 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
.collect::<Result<Vec<_>, _>>()?, .collect::<Result<Vec<_>, _>>()?,
}; };
// Log the success // Log the success
trace!("Authenticated to namespace `{}` with token `{}`", ns, tk); trace!("Authenticated to namespace `{}` with access method `{}`", ns, ac);
// Set the session // Set the session
session.tk = Some(value); session.tk = Some(value);
session.ns = Some(ns.to_owned()); session.ns = Some(ns.to_owned());
session.ac = Some(ac.to_owned());
session.exp = token_data.claims.exp; session.exp = token_data.claims.exp;
session.au = session.au =
Arc::new(Auth::new(Actor::new(de.name.to_string(), roles, Level::Namespace(ns)))); Arc::new(Auth::new(Actor::new(de.name.to_string(), roles, Level::Namespace(ns))));
Ok(()) Ok(())
} }
// Check if this is namespace authentication // Check if this is namespace authentication with user credentials
Claims { Claims {
ns: Some(ns), ns: Some(ns),
id: Some(id), id: Some(id),
@ -419,7 +403,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
trace!("Error while authenticating to namespace `{ns}`: {e}"); trace!("Error while authenticating to namespace `{ns}`: {e}");
Error::InvalidAuth Error::InvalidAuth
})?; })?;
let cf = config_alg(Algorithm::Hs512, de.code)?; let cf = config(Algorithm::Hs512, de.code)?;
// Verify the token // Verify the token
decode::<Claims>(token, &cf.0, &cf.1)?; decode::<Claims>(token, &cf.0, &cf.1)?;
// Log the success // Log the success
@ -435,7 +419,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
))); )));
Ok(()) Ok(())
} }
// Check if this is root level authentication // Check if this is root authentication with user credentials
Claims { Claims {
id: Some(id), id: Some(id),
.. ..
@ -449,7 +433,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
trace!("Error while authenticating to root: {e}"); trace!("Error while authenticating to root: {e}");
Error::InvalidAuth Error::InvalidAuth
})?; })?;
let cf = config_alg(Algorithm::Hs512, de.code)?; let cf = config(Algorithm::Hs512, de.code)?;
// Verify the token // Verify the token
decode::<Claims>(token, &cf.0, &cf.1)?; decode::<Claims>(token, &cf.0, &cf.1)?;
// Log the success // Log the success
@ -814,7 +798,7 @@ mod tests {
iat: Some(Utc::now().timestamp()), iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()), exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
tk: Some("token".to_string()), ac: Some("token".to_string()),
ns: Some("test".to_string()), ns: Some("test".to_string()),
..Claims::default() ..Claims::default()
}; };
@ -822,7 +806,7 @@ mod tests {
let ds = Datastore::new("memory").await.unwrap(); let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
format!("DEFINE TOKEN token ON NS TYPE HS512 VALUE '{secret}'").as_str(), format!("DEFINE ACCESS token ON NS TYPE JWT ALGORITHM HS512 KEY '{secret}'").as_str(),
&sess, &sess,
None, None,
) )
@ -921,7 +905,7 @@ mod tests {
iat: Some(Utc::now().timestamp()), iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()), exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
tk: Some("token".to_string()), ac: Some("token".to_string()),
ns: Some("test".to_string()), ns: Some("test".to_string()),
db: Some("test".to_string()), db: Some("test".to_string()),
..Claims::default() ..Claims::default()
@ -930,7 +914,8 @@ mod tests {
let ds = Datastore::new("memory").await.unwrap(); let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
format!("DEFINE TOKEN token ON DB TYPE HS512 VALUE '{secret}'").as_str(), format!("DEFINE ACCESS token ON DATABASE TYPE JWT ALGORITHM HS512 KEY '{secret}'")
.as_str(),
&sess, &sess,
None, None,
) )
@ -1023,7 +1008,7 @@ mod tests {
} }
#[tokio::test] #[tokio::test]
async fn test_token_scope() { async fn test_token_db_record() {
let secret = "jwt_secret"; let secret = "jwt_secret";
let key = EncodingKey::from_secret(secret.as_ref()); let key = EncodingKey::from_secret(secret.as_ref());
let claims = Claims { let claims = Claims {
@ -1031,17 +1016,25 @@ mod tests {
iat: Some(Utc::now().timestamp()), iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()), exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
tk: Some("token".to_string()),
ns: Some("test".to_string()), ns: Some("test".to_string()),
db: Some("test".to_string()), db: Some("test".to_string()),
sc: Some("test".to_string()), ac: Some("token".to_string()),
id: Some("user:test".to_string()),
..Claims::default() ..Claims::default()
}; };
let ds = Datastore::new("memory").await.unwrap(); let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
format!("DEFINE TOKEN token ON SCOPE test TYPE HS512 VALUE '{secret}';").as_str(), format!(
r#"
DEFINE ACCESS token ON DATABASE TYPE RECORD
WITH JWT ALGORITHM HS512 KEY '{secret}';
CREATE user:test;
"#
)
.as_str(),
&sess, &sess,
None, None,
) )
@ -1050,7 +1043,7 @@ mod tests {
// //
// Test without roles defined // Test without roles defined
// Roles should be ignored in scope authentication // Roles should be ignored in record access
// //
{ {
// Prepare the claims object // Prepare the claims object
@ -1065,9 +1058,9 @@ mod tests {
assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.sc, Some("test".to_string())); assert_eq!(sess.ac, Some("token".to_string()));
assert_eq!(sess.au.id(), "token"); assert_eq!(sess.au.id(), "user:test");
assert!(sess.au.is_scope()); assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test")); assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test")); assert_eq!(sess.au.level().db(), Some("test"));
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
@ -1078,7 +1071,7 @@ mod tests {
// //
// Test with roles defined // Test with roles defined
// Roles should be ignored in scope authentication // Roles should be ignored in record access
// //
{ {
// Prepare the claims object // Prepare the claims object
@ -1093,9 +1086,9 @@ mod tests {
assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.sc, Some("test".to_string())); assert_eq!(sess.ac, Some("token".to_string()));
assert_eq!(sess.au.id(), "token"); assert_eq!(sess.au.id(), "user:test");
assert!(sess.au.is_scope()); assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test")); assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test")); assert_eq!(sess.au.level().db(), Some("test"));
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
@ -1121,12 +1114,12 @@ mod tests {
} }
// //
// Test with valid token invalid sc // Test with valid token invalid access method
// //
{ {
// Prepare the claims object // Prepare the claims object
let mut claims = claims.clone(); let mut claims = claims.clone();
claims.sc = Some("invalid".to_string()); claims.ac = Some("invalid".to_string());
// Create the token // Create the token
let enc = encode(&HEADER, &claims, &key).unwrap(); let enc = encode(&HEADER, &claims, &key).unwrap();
// Signin with the token // Signin with the token
@ -1156,7 +1149,7 @@ mod tests {
// Test with generic user identifier // Test with generic user identifier
// //
{ {
let resource_id = "user:`2k9qnabxuxh8k4d5gfto`".to_string(); let resource_id = "user:⟨2k9qnabxuxh8k4d5gfto⟩".to_string();
// Prepare the claims object // Prepare the claims object
let mut claims = claims.clone(); let mut claims = claims.clone();
claims.id = Some(resource_id.clone()); claims.id = Some(resource_id.clone());
@ -1169,11 +1162,11 @@ mod tests {
assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.sc, Some("test".to_string())); assert_eq!(sess.ac, Some("token".to_string()));
assert_eq!(sess.au.id(), "token"); assert_eq!(sess.au.id(), resource_id);
assert!(sess.au.is_scope()); assert!(sess.au.is_record());
let user_id = syn::thing(&resource_id).unwrap(); let user_id = syn::thing(&resource_id).unwrap();
assert_eq!(sess.sd, Some(Value::from(user_id))); assert_eq!(sess.rd, Some(Value::from(user_id)));
} }
// //
@ -1195,11 +1188,11 @@ mod tests {
assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.sc, Some("test".to_string())); assert_eq!(sess.ac, Some("token".to_string()));
assert_eq!(sess.au.id(), "token"); assert_eq!(sess.au.id(), resource_id);
assert!(sess.au.is_scope()); assert!(sess.au.is_record());
let user_id = syn::thing(&resource_id).unwrap(); let user_id = syn::thing(&resource_id).unwrap();
assert_eq!(sess.sd, Some(Value::from(user_id))); assert_eq!(sess.rd, Some(Value::from(user_id)));
} }
} }
@ -1222,11 +1215,11 @@ mod tests {
assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.sc, Some("test".to_string())); assert_eq!(sess.ac, Some("token".to_string()));
assert_eq!(sess.au.id(), "token"); assert_eq!(sess.au.id(), resource_id);
assert!(sess.au.is_scope()); assert!(sess.au.is_record());
let user_id = syn::thing(&resource_id).unwrap(); let user_id = syn::thing(&resource_id).unwrap();
assert_eq!(sess.sd, Some(Value::from(user_id))); assert_eq!(sess.rd, Some(Value::from(user_id)));
} }
} }
@ -1250,11 +1243,11 @@ mod tests {
assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.sc, Some("test".to_string())); assert_eq!(sess.ac, Some("token".to_string()));
assert_eq!(sess.au.id(), "token"); assert_eq!(sess.au.id(), resource_id);
assert!(sess.au.is_scope()); assert!(sess.au.is_record());
let user_id = syn::thing(&resource_id).unwrap(); let user_id = syn::thing(&resource_id).unwrap();
assert_eq!(sess.sd, Some(Value::from(user_id))); assert_eq!(sess.rd, Some(Value::from(user_id)));
} }
} }
@ -1277,16 +1270,16 @@ mod tests {
assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.sc, Some("test".to_string())); assert_eq!(sess.ac, Some("token".to_string()));
assert_eq!(sess.au.id(), "token"); assert_eq!(sess.au.id(), resource_id);
assert!(sess.au.is_scope()); assert!(sess.au.is_record());
let user_id = syn::thing(&resource_id).unwrap(); let user_id = syn::thing(&resource_id).unwrap();
assert_eq!(sess.sd, Some(Value::from(user_id))); assert_eq!(sess.rd, Some(Value::from(user_id)));
} }
} }
#[tokio::test] #[tokio::test]
async fn test_token_scope_custom_claims() { async fn test_token_db_record_custom_claims() {
use std::collections::HashMap; use std::collections::HashMap;
let secret = "jwt_secret"; let secret = "jwt_secret";
@ -1295,7 +1288,15 @@ mod tests {
let ds = Datastore::new("memory").await.unwrap(); let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
format!("DEFINE TOKEN token ON SCOPE test TYPE HS512 VALUE '{secret}';").as_str(), format!(
r#"
DEFINE ACCESS token ON DATABASE TYPE RECORD
WITH JWT ALGORITHM HS512 KEY '{secret}';
CREATE user:test;
"#
)
.as_str(),
&sess, &sess,
None, None,
) )
@ -1315,10 +1316,10 @@ mod tests {
"iat": {now}, "iat": {now},
"nbf": {now}, "nbf": {now},
"exp": {later}, "exp": {later},
"tk": "token",
"ns": "test", "ns": "test",
"db": "test", "db": "test",
"sc": "test", "ac": "token",
"id": "user:test",
"string_claim": "test", "string_claim": "test",
"bool_claim": true, "bool_claim": true,
@ -1351,9 +1352,9 @@ mod tests {
assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.sc, Some("test".to_string())); assert_eq!(sess.ac, Some("token".to_string()));
assert_eq!(sess.au.id(), "token"); assert_eq!(sess.au.id(), "user:test");
assert!(sess.au.is_scope()); assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test")); assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test")); assert_eq!(sess.au.level().db(), Some("test"));
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
@ -1387,7 +1388,7 @@ mod tests {
#[cfg(feature = "jwks")] #[cfg(feature = "jwks")]
#[tokio::test] #[tokio::test]
async fn test_token_scope_jwks() { async fn test_token_db_record_jwks() {
use crate::dbs::capabilities::{Capabilities, NetTarget, Targets}; use crate::dbs::capabilities::{Capabilities, NetTarget, Targets};
use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine}; use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine};
use jsonwebtoken::jwk::{Jwk, JwkSet}; use jsonwebtoken::jwk::{Jwk, JwkSet};
@ -1448,8 +1449,15 @@ mod tests {
let sess = Session::owner().with_ns("test").with_db("test"); let sess = Session::owner().with_ns("test").with_db("test");
ds.execute( ds.execute(
format!("DEFINE TOKEN token ON SCOPE test TYPE JWKS VALUE '{server_url}/{jwks_path}';") format!(
.as_str(), r#"
DEFINE ACCESS token ON DATABASE TYPE RECORD
WITH JWT URL '{server_url}/{jwks_path}';
CREATE user:test;
"#
)
.as_str(),
&sess, &sess,
None, None,
) )
@ -1470,16 +1478,16 @@ mod tests {
iat: Some(Utc::now().timestamp()), iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()), exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
tk: Some("token".to_string()),
ns: Some("test".to_string()), ns: Some("test".to_string()),
db: Some("test".to_string()), db: Some("test".to_string()),
sc: Some("test".to_string()), ac: Some("token".to_string()),
id: Some("user:test".to_string()),
..Claims::default() ..Claims::default()
}; };
// //
// Test without roles defined // Test without roles defined
// Roles should be ignored in scope authentication // Roles should be ignored in record access
// //
{ {
// Prepare the claims object // Prepare the claims object
@ -1494,9 +1502,9 @@ mod tests {
assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.sc, Some("test".to_string())); assert_eq!(sess.ac, Some("token".to_string()));
assert_eq!(sess.au.id(), "token"); assert_eq!(sess.au.id(), "user:test");
assert!(sess.au.is_scope()); assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test")); assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test")); assert_eq!(sess.au.level().db(), Some("test"));
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");

View file

@ -1,12 +1,12 @@
/// Stores a DEFINE ACCESS ON DATABASE config definition
use crate::key::error::KeyCategory; use crate::key::error::KeyCategory;
use crate::key::key_req::KeyRequirements; use crate::key::key_req::KeyRequirements;
/// Stores a DEFINE TOKEN ON DATABASE config definition
use derive::Key; use derive::Key;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)] #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)]
#[non_exhaustive] #[non_exhaustive]
pub struct Tk<'a> { pub struct Ac<'a> {
__: u8, __: u8,
_a: u8, _a: u8,
pub ns: &'a str, pub ns: &'a str,
@ -15,33 +15,33 @@ pub struct Tk<'a> {
_c: u8, _c: u8,
_d: u8, _d: u8,
_e: u8, _e: u8,
pub tk: &'a str, pub ac: &'a str,
} }
pub fn new<'a>(ns: &'a str, db: &'a str, tk: &'a str) -> Tk<'a> { pub fn new<'a>(ns: &'a str, db: &'a str, ac: &'a str) -> Ac<'a> {
Tk::new(ns, db, tk) Ac::new(ns, db, ac)
} }
pub fn prefix(ns: &str, db: &str) -> Vec<u8> { pub fn prefix(ns: &str, db: &str) -> Vec<u8> {
let mut k = super::all::new(ns, db).encode().unwrap(); let mut k = super::all::new(ns, db).encode().unwrap();
k.extend_from_slice(&[b'!', b't', b'k', 0x00]); k.extend_from_slice(&[b'!', b'a', b'c', 0x00]);
k k
} }
pub fn suffix(ns: &str, db: &str) -> Vec<u8> { pub fn suffix(ns: &str, db: &str) -> Vec<u8> {
let mut k = super::all::new(ns, db).encode().unwrap(); let mut k = super::all::new(ns, db).encode().unwrap();
k.extend_from_slice(&[b'!', b't', b'k', 0xff]); k.extend_from_slice(&[b'!', b'a', b'c', 0xff]);
k k
} }
impl KeyRequirements for Tk<'_> { impl KeyRequirements for Ac<'_> {
fn key_category(&self) -> KeyCategory { fn key_category(&self) -> KeyCategory {
KeyCategory::DatabaseToken KeyCategory::DatabaseAccess
} }
} }
impl<'a> Tk<'a> { impl<'a> Ac<'a> {
pub fn new(ns: &'a str, db: &'a str, tk: &'a str) -> Self { pub fn new(ns: &'a str, db: &'a str, ac: &'a str) -> Self {
Self { Self {
__: b'/', __: b'/',
_a: b'*', _a: b'*',
@ -49,9 +49,9 @@ impl<'a> Tk<'a> {
_b: b'*', _b: b'*',
db, db,
_c: b'!', _c: b'!',
_d: b't', _d: b'a',
_e: b'k', _e: b'c',
tk, ac,
} }
} }
} }
@ -62,27 +62,27 @@ mod tests {
fn key() { fn key() {
use super::*; use super::*;
#[rustfmt::skip] #[rustfmt::skip]
let val = Tk::new( let val = Ac::new(
"testns", "testns",
"testdb", "testdb",
"testtk", "testac",
); );
let enc = Tk::encode(&val).unwrap(); let enc = Ac::encode(&val).unwrap();
assert_eq!(enc, b"/*testns\x00*testdb\x00!tktesttk\x00"); assert_eq!(enc, b"/*testns\x00*testdb\x00!actestac\x00");
let dec = Tk::decode(&enc).unwrap(); let dec = Ac::decode(&enc).unwrap();
assert_eq!(val, dec); assert_eq!(val, dec);
} }
#[test] #[test]
fn test_prefix() { fn test_prefix() {
let val = super::prefix("testns", "testdb"); let val = super::prefix("testns", "testdb");
assert_eq!(val, b"/*testns\0*testdb\0!tk\0"); assert_eq!(val, b"/*testns\0*testdb\0!ac\0");
} }
#[test] #[test]
fn test_suffix() { fn test_suffix() {
let val = super::suffix("testns", "testdb"); let val = super::suffix("testns", "testdb");
assert_eq!(val, b"/*testns\0*testdb\0!tk\xff"); assert_eq!(val, b"/*testns\0*testdb\0!ac\xff");
} }
} }

View file

@ -1,12 +1,11 @@
pub mod ac;
pub mod all; pub mod all;
pub mod az; pub mod az;
pub mod fc; pub mod fc;
pub mod ml; pub mod ml;
pub mod pa; pub mod pa;
pub mod sc;
pub mod tb; pub mod tb;
pub mod ti; pub mod ti;
pub mod tk;
pub mod ts; pub mod ts;
pub mod us; pub mod us;
pub mod vs; pub mod vs;

View file

@ -1,76 +0,0 @@
//! Stores a DEFINE SCOPE config definition
use crate::key::error::KeyCategory;
use crate::key::key_req::KeyRequirements;
use derive::Key;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)]
#[non_exhaustive]
pub struct Sc<'a> {
__: u8,
_a: u8,
pub ns: &'a str,
_b: u8,
pub db: &'a str,
_c: u8,
_d: u8,
_e: u8,
pub sc: &'a str,
}
pub fn new<'a>(ns: &'a str, db: &'a str, sc: &'a str) -> Sc<'a> {
Sc::new(ns, db, sc)
}
pub fn prefix(ns: &str, db: &str) -> Vec<u8> {
let mut k = super::all::new(ns, db).encode().unwrap();
k.extend_from_slice(&[b'!', b's', b'c', 0x00]);
k
}
pub fn suffix(ns: &str, db: &str) -> Vec<u8> {
let mut k = super::all::new(ns, db).encode().unwrap();
k.extend_from_slice(&[b'!', b's', b'c', 0xff]);
k
}
impl KeyRequirements for Sc<'_> {
fn key_category(&self) -> KeyCategory {
KeyCategory::DatabaseScope
}
}
impl<'a> Sc<'a> {
pub fn new(ns: &'a str, db: &'a str, sc: &'a str) -> Self {
Self {
__: b'/',
_a: b'*',
ns,
_b: b'*',
db,
_c: b'!',
_d: b's',
_e: b'c',
sc,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn key() {
use super::*;
#[rustfmt::skip]
let val = Sc::new(
"testns",
"testdb",
"testsc",
);
let enc = Sc::encode(&val).unwrap();
assert_eq!(enc, b"/*testns\0*testdb\0!sctestsc\0");
let dec = Sc::decode(&enc).unwrap();
assert_eq!(val, dec);
}
}

View file

@ -8,6 +8,8 @@ pub enum KeyCategory {
Unknown, Unknown,
/// crate::key::root::all / /// crate::key::root::all /
Root, Root,
/// crate::key::root::ac /!ac{ac}
Access,
/// crate::key::root::hb /!hb{ts}/{nd} /// crate::key::root::hb /!hb{ts}/{nd}
Heartbeat, Heartbeat,
/// crate::key::root::nd /!nd{nd} /// crate::key::root::nd /!nd{nd}
@ -32,13 +34,15 @@ pub enum KeyCategory {
DatabaseIdentifier, DatabaseIdentifier,
/// crate::key::namespace::lg /*{ns}!lg{lg} /// crate::key::namespace::lg /*{ns}!lg{lg}
DatabaseLogAlias, DatabaseLogAlias,
/// crate::key::namespace::tk /*{ns}!tk{tk} /// crate::key::namespace::ac /*{ns}!ac{ac}
NamespaceToken, NamespaceAccess,
/// crate::key::namespace::us /*{ns}!us{us} /// crate::key::namespace::us /*{ns}!us{us}
NamespaceUser, NamespaceUser,
/// ///
/// crate::key::database::all /*{ns}*{db} /// crate::key::database::all /*{ns}*{db}
DatabaseRoot, DatabaseRoot,
/// crate::key::database::ac /*{ns}*{db}!ac{ac}
DatabaseAccess,
/// crate::key::database::az /*{ns}*{db}!az{az} /// crate::key::database::az /*{ns}*{db}!az{az}
DatabaseAnalyzer, DatabaseAnalyzer,
/// crate::key::database::fc /*{ns}*{db}!fn{fc} /// crate::key::database::fc /*{ns}*{db}!fn{fc}
@ -49,14 +53,10 @@ pub enum KeyCategory {
DatabaseModel, DatabaseModel,
/// crate::key::database::pa /*{ns}*{db}!pa{pa} /// crate::key::database::pa /*{ns}*{db}!pa{pa}
DatabaseParameter, DatabaseParameter,
/// crate::key::database::sc /*{ns}*{db}!sc{sc}
DatabaseScope,
/// crate::key::database::tb /*{ns}*{db}!tb{tb} /// crate::key::database::tb /*{ns}*{db}!tb{tb}
DatabaseTable, DatabaseTable,
/// crate::key::database::ti /+{ns id}*{db id}!ti /// crate::key::database::ti /+{ns id}*{db id}!ti
DatabaseTableIdentifier, DatabaseTableIdentifier,
/// crate::key::database::tk /*{ns}*{db}!tk{tk}
DatabaseToken,
/// crate::key::database::ts /*{ns}*{db}!ts{ts} /// crate::key::database::ts /*{ns}*{db}!ts{ts}
DatabaseTimestamp, DatabaseTimestamp,
/// crate::key::database::us /*{ns}*{db}!us{us} /// crate::key::database::us /*{ns}*{db}!us{us}
@ -64,11 +64,6 @@ pub enum KeyCategory {
/// crate::key::database::vs /*{ns}*{db}!vs /// crate::key::database::vs /*{ns}*{db}!vs
DatabaseVersionstamp, DatabaseVersionstamp,
/// ///
/// crate::key::scope::all /*{ns}*{db}±{sc}
ScopeRoot,
/// crate::key::scope::tk /*{ns}*{db}±{sc}!tk{tk}
ScopeToken,
///
/// crate::key::table::all /*{ns}*{db}*{tb} /// crate::key::table::all /*{ns}*{db}*{tb}
TableRoot, TableRoot,
/// crate::key::table::ev /*{ns}*{db}*{tb}!ev{ev} /// crate::key::table::ev /*{ns}*{db}*{tb}!ev{ev}
@ -124,6 +119,7 @@ impl Display for KeyCategory {
let name = match self { let name = match self {
KeyCategory::Unknown => "Unknown", KeyCategory::Unknown => "Unknown",
KeyCategory::Root => "Root", KeyCategory::Root => "Root",
KeyCategory::Access => "Access",
KeyCategory::Heartbeat => "Heartbeat", KeyCategory::Heartbeat => "Heartbeat",
KeyCategory::Node => "Node", KeyCategory::Node => "Node",
KeyCategory::NamespaceIdentifier => "NamespaceIdentifier", KeyCategory::NamespaceIdentifier => "NamespaceIdentifier",
@ -135,23 +131,20 @@ impl Display for KeyCategory {
KeyCategory::DatabaseAlias => "DatabaseAlias", KeyCategory::DatabaseAlias => "DatabaseAlias",
KeyCategory::DatabaseIdentifier => "DatabaseIdentifier", KeyCategory::DatabaseIdentifier => "DatabaseIdentifier",
KeyCategory::DatabaseLogAlias => "DatabaseLogAlias", KeyCategory::DatabaseLogAlias => "DatabaseLogAlias",
KeyCategory::NamespaceToken => "NamespaceToken", KeyCategory::NamespaceAccess => "NamespaceAccess",
KeyCategory::NamespaceUser => "NamespaceUser", KeyCategory::NamespaceUser => "NamespaceUser",
KeyCategory::DatabaseRoot => "DatabaseRoot", KeyCategory::DatabaseRoot => "DatabaseRoot",
KeyCategory::DatabaseAccess => "DatabaseAccess",
KeyCategory::DatabaseAnalyzer => "DatabaseAnalyzer", KeyCategory::DatabaseAnalyzer => "DatabaseAnalyzer",
KeyCategory::DatabaseFunction => "DatabaseFunction", KeyCategory::DatabaseFunction => "DatabaseFunction",
KeyCategory::DatabaseLog => "DatabaseLog", KeyCategory::DatabaseLog => "DatabaseLog",
KeyCategory::DatabaseModel => "DatabaseModel", KeyCategory::DatabaseModel => "DatabaseModel",
KeyCategory::DatabaseParameter => "DatabaseParameter", KeyCategory::DatabaseParameter => "DatabaseParameter",
KeyCategory::DatabaseScope => "DatabaseScope",
KeyCategory::DatabaseTable => "DatabaseTable", KeyCategory::DatabaseTable => "DatabaseTable",
KeyCategory::DatabaseTableIdentifier => "DatabaseTableIdentifier", KeyCategory::DatabaseTableIdentifier => "DatabaseTableIdentifier",
KeyCategory::DatabaseToken => "DatabaseToken",
KeyCategory::DatabaseTimestamp => "DatabaseTimestamp", KeyCategory::DatabaseTimestamp => "DatabaseTimestamp",
KeyCategory::DatabaseUser => "DatabaseUser", KeyCategory::DatabaseUser => "DatabaseUser",
KeyCategory::DatabaseVersionstamp => "DatabaseVersionstamp", KeyCategory::DatabaseVersionstamp => "DatabaseVersionstamp",
KeyCategory::ScopeRoot => "ScopeRoot",
KeyCategory::ScopeToken => "ScopeToken",
KeyCategory::TableRoot => "TableRoot", KeyCategory::TableRoot => "TableRoot",
KeyCategory::TableEvent => "TableEvent", KeyCategory::TableEvent => "TableEvent",
KeyCategory::TableField => "TableField", KeyCategory::TableField => "TableField",

View file

@ -11,28 +11,24 @@
/// crate::key::node::lq /${nd}!lq{lq}{ns}{db} /// crate::key::node::lq /${nd}!lq{lq}{ns}{db}
/// ///
/// crate::key::namespace::all /*{ns} /// crate::key::namespace::all /*{ns}
/// crate::key::namespace::ac /*{ns}!ac{ac}
/// crate::key::namespace::db /*{ns}!db{db} /// crate::key::namespace::db /*{ns}!db{db}
/// crate::key::namespace::di /+{ns id}!di /// crate::key::namespace::di /+{ns id}!di
/// crate::key::namespace::lg /*{ns}!lg{lg} /// crate::key::namespace::lg /*{ns}!lg{lg}
/// crate::key::namespace::tk /*{ns}!tk{tk}
/// crate::key::namespace::us /*{ns}!us{us} /// crate::key::namespace::us /*{ns}!us{us}
/// ///
/// crate::key::database::all /*{ns}*{db} /// crate::key::database::all /*{ns}*{db}
/// crate::key::database::ac /*{ns}*{db}!ac{ac}
/// crate::key::database::az /*{ns}*{db}!az{az} /// crate::key::database::az /*{ns}*{db}!az{az}
/// crate::key::database::fc /*{ns}*{db}!fn{fc} /// crate::key::database::fc /*{ns}*{db}!fn{fc}
/// crate::key::database::lg /*{ns}*{db}!lg{lg} /// crate::key::database::lg /*{ns}*{db}!lg{lg}
/// crate::key::database::pa /*{ns}*{db}!pa{pa} /// crate::key::database::pa /*{ns}*{db}!pa{pa}
/// crate::key::database::sc /*{ns}*{db}!sc{sc}
/// crate::key::database::tb /*{ns}*{db}!tb{tb} /// crate::key::database::tb /*{ns}*{db}!tb{tb}
/// crate::key::database::ti /+{ns id}*{db id}!ti /// crate::key::database::ti /+{ns id}*{db id}!ti
/// crate::key::database::tk /*{ns}*{db}!tk{tk}
/// crate::key::database::ts /*{ns}*{db}!ts{ts} /// crate::key::database::ts /*{ns}*{db}!ts{ts}
/// crate::key::database::us /*{ns}*{db}!us{us} /// crate::key::database::us /*{ns}*{db}!us{us}
/// crate::key::database::vs /*{ns}*{db}!vs /// crate::key::database::vs /*{ns}*{db}!vs
/// ///
/// crate::key::scope::all /*{ns}*{db}±{sc}
/// crate::key::scope::tk /*{ns}*{db}±{sc}!tk{tk}
///
/// crate::key::table::all /*{ns}*{db}*{tb} /// crate::key::table::all /*{ns}*{db}*{tb}
/// crate::key::table::ev /*{ns}*{db}*{tb}!ev{ev} /// crate::key::table::ev /*{ns}*{db}*{tb}!ev{ev}
/// crate::key::table::fd /*{ns}*{db}*{tb}!fd{fd} /// crate::key::table::fd /*{ns}*{db}*{tb}!fd{fd}
@ -70,6 +66,5 @@ pub(crate) mod key_req;
pub mod namespace; pub mod namespace;
pub mod node; pub mod node;
pub mod root; pub mod root;
pub mod scope;
pub mod table; pub mod table;
pub mod thing; pub mod thing;

View file

@ -1,4 +1,4 @@
//! Stores a DEFINE TOKEN ON NAMESPACE config definition //! Stores a DEFINE ACCESS ON NAMESPACE config definition
use crate::key::error::KeyCategory; use crate::key::error::KeyCategory;
use crate::key::key_req::KeyRequirements; use crate::key::key_req::KeyRequirements;
use derive::Key; use derive::Key;
@ -6,48 +6,48 @@ use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)] #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)]
#[non_exhaustive] #[non_exhaustive]
pub struct Tk<'a> { pub struct Ac<'a> {
__: u8, __: u8,
_a: u8, _a: u8,
pub ns: &'a str, pub ns: &'a str,
_b: u8, _b: u8,
_c: u8, _c: u8,
_d: u8, _d: u8,
pub tk: &'a str, pub ac: &'a str,
} }
pub fn new<'a>(ns: &'a str, tk: &'a str) -> Tk<'a> { pub fn new<'a>(ns: &'a str, ac: &'a str) -> Ac<'a> {
Tk::new(ns, tk) Ac::new(ns, ac)
} }
pub fn prefix(ns: &str) -> Vec<u8> { pub fn prefix(ns: &str) -> Vec<u8> {
let mut k = super::all::new(ns).encode().unwrap(); let mut k = super::all::new(ns).encode().unwrap();
k.extend_from_slice(&[b'!', b't', b'k', 0x00]); k.extend_from_slice(&[b'!', b'a', b'c', 0x00]);
k k
} }
pub fn suffix(ns: &str) -> Vec<u8> { pub fn suffix(ns: &str) -> Vec<u8> {
let mut k = super::all::new(ns).encode().unwrap(); let mut k = super::all::new(ns).encode().unwrap();
k.extend_from_slice(&[b'!', b't', b'k', 0xff]); k.extend_from_slice(&[b'!', b'a', b'c', 0xff]);
k k
} }
impl KeyRequirements for Tk<'_> { impl KeyRequirements for Ac<'_> {
fn key_category(&self) -> KeyCategory { fn key_category(&self) -> KeyCategory {
KeyCategory::NamespaceToken KeyCategory::NamespaceAccess
} }
} }
impl<'a> Tk<'a> { impl<'a> Ac<'a> {
pub fn new(ns: &'a str, tk: &'a str) -> Self { pub fn new(ns: &'a str, ac: &'a str) -> Self {
Self { Self {
__: b'/', __: b'/',
_a: b'*', _a: b'*',
ns, ns,
_b: b'!', _b: b'!',
_c: b't', _c: b'a',
_d: b'k', _d: b'c',
tk, ac,
} }
} }
} }
@ -58,13 +58,13 @@ mod tests {
fn key() { fn key() {
use super::*; use super::*;
#[rustfmt::skip] #[rustfmt::skip]
let val = Tk::new( let val = Ac::new(
"testns", "testns",
"testtk", "testac",
); );
let enc = Tk::encode(&val).unwrap(); let enc = Ac::encode(&val).unwrap();
assert_eq!(enc, b"/*testns\0!tktesttk\0"); assert_eq!(enc, b"/*testns\0!actestac\0");
let dec = Tk::decode(&enc).unwrap(); let dec = Ac::decode(&enc).unwrap();
assert_eq!(val, dec); assert_eq!(val, dec);
} }
} }

View file

@ -1,5 +1,5 @@
pub mod ac;
pub mod all; pub mod all;
pub mod db; pub mod db;
pub mod di; pub mod di;
pub mod tk;
pub mod us; pub mod us;

74
core/src/key/root/ac.rs Normal file
View file

@ -0,0 +1,74 @@
use crate::key::error::KeyCategory;
use crate::key::key_req::KeyRequirements;
use derive::Key;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)]
#[non_exhaustive]
pub struct Ac<'a> {
__: u8,
_a: u8,
_b: u8,
_c: u8,
pub ac: &'a str,
}
pub fn new(ac: &str) -> Ac<'_> {
Ac::new(ac)
}
pub fn prefix() -> Vec<u8> {
let mut k = super::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();
k.extend_from_slice(&[b'!', b'a', b'c', 0xff]);
k
}
impl KeyRequirements for Ac<'_> {
fn key_category(&self) -> KeyCategory {
KeyCategory::Access
}
}
impl<'a> Ac<'a> {
pub fn new(ac: &'a str) -> Self {
Self {
__: b'/',
_a: b'!',
_b: b'a',
_c: b'c',
ac,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn key() {
use super::*;
#[rustfmt::skip]
let val = Ac::new("testac");
let enc = Ac::encode(&val).unwrap();
assert_eq!(enc, b"/!actestac\x00");
let dec = Ac::decode(&enc).unwrap();
assert_eq!(val, dec);
}
#[test]
fn test_prefix() {
let val = super::prefix();
assert_eq!(val, b"/!ac\0");
}
#[test]
fn test_suffix() {
let val = super::suffix();
assert_eq!(val, b"/!ac\xff");
}
}

View file

@ -1,3 +1,4 @@
pub mod ac;
pub mod all; pub mod all;
pub mod hb; pub mod hb;
pub mod nd; pub mod nd;

View file

@ -1,60 +0,0 @@
//! Stores the key prefix for all keys under a scope
use crate::key::error::KeyCategory;
use crate::key::key_req::KeyRequirements;
use derive::Key;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)]
#[non_exhaustive]
pub struct Scope<'a> {
__: u8,
_a: u8,
pub ns: &'a str,
_b: u8,
pub db: &'a str,
_c: u8,
pub sc: &'a str,
}
pub fn new<'a>(ns: &'a str, db: &'a str, sc: &'a str) -> Scope<'a> {
Scope::new(ns, db, sc)
}
impl KeyRequirements for Scope<'_> {
fn key_category(&self) -> KeyCategory {
KeyCategory::ScopeRoot
}
}
impl<'a> Scope<'a> {
pub fn new(ns: &'a str, db: &'a str, sc: &'a str) -> Self {
Self {
__: b'/',
_a: b'*',
ns,
_b: b'*',
db,
_c: super::CHAR,
sc,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn key() {
use super::*;
#[rustfmt::skip]
let val = Scope::new(
"testns",
"testdb",
"testsc",
);
let enc = Scope::encode(&val).unwrap();
assert_eq!(enc, b"/*testns\0*testdb\0\xb1testsc\0");
let dec = Scope::decode(&enc).unwrap();
assert_eq!(val, dec);
}
}

View file

@ -1,4 +0,0 @@
pub mod all;
pub mod tk;
const CHAR: u8 = 0xb1; // ±

View file

@ -1,81 +0,0 @@
//! Stores a DEFINE TOKEN ON SCOPE config definition
use crate::key::error::KeyCategory;
use crate::key::key_req::KeyRequirements;
use derive::Key;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)]
#[non_exhaustive]
pub struct Tk<'a> {
__: u8,
_a: u8,
pub ns: &'a str,
_b: u8,
pub db: &'a str,
_c: u8,
pub sc: &'a str,
_d: u8,
_e: u8,
_f: u8,
pub tk: &'a str,
}
pub fn new<'a>(ns: &'a str, db: &'a str, sc: &'a str, tk: &'a str) -> Tk<'a> {
Tk::new(ns, db, sc, tk)
}
pub fn prefix(ns: &str, db: &str, sc: &str) -> Vec<u8> {
let mut k = super::all::new(ns, db, sc).encode().unwrap();
k.extend_from_slice(&[b'!', b't', b'k', 0x00]);
k
}
pub fn suffix(ns: &str, db: &str, sc: &str) -> Vec<u8> {
let mut k = super::all::new(ns, db, sc).encode().unwrap();
k.extend_from_slice(&[b'!', b't', b'k', 0xff]);
k
}
impl KeyRequirements for Tk<'_> {
fn key_category(&self) -> KeyCategory {
KeyCategory::ScopeToken
}
}
impl<'a> Tk<'a> {
pub fn new(ns: &'a str, db: &'a str, sc: &'a str, tk: &'a str) -> Self {
Self {
__: b'/',
_a: b'*',
ns,
_b: b'*',
db,
_c: super::CHAR,
sc,
_d: b'!',
_e: b't',
_f: b'k',
tk,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn key() {
use super::*;
#[rustfmt::skip]
let val = Tk::new(
"testns",
"testdb",
"testsc",
"testtk",
);
let enc = Tk::encode(&val).unwrap();
assert_eq!(enc, b"/*testns\0*testdb\0\xb1testsc\0!tktesttk\0");
let dec = Tk::decode(&enc).unwrap();
assert_eq!(val, dec);
}
}

View file

@ -1,5 +1,6 @@
use crate::idg::u32::U32; use crate::idg::u32::U32;
use crate::kvs::kv::Key; use crate::kvs::kv::Key;
use crate::sql::statements::DefineAccessStatement;
use crate::sql::statements::DefineAnalyzerStatement; use crate::sql::statements::DefineAnalyzerStatement;
use crate::sql::statements::DefineDatabaseStatement; use crate::sql::statements::DefineDatabaseStatement;
use crate::sql::statements::DefineEventStatement; use crate::sql::statements::DefineEventStatement;
@ -9,9 +10,7 @@ use crate::sql::statements::DefineIndexStatement;
use crate::sql::statements::DefineModelStatement; use crate::sql::statements::DefineModelStatement;
use crate::sql::statements::DefineNamespaceStatement; use crate::sql::statements::DefineNamespaceStatement;
use crate::sql::statements::DefineParamStatement; use crate::sql::statements::DefineParamStatement;
use crate::sql::statements::DefineScopeStatement;
use crate::sql::statements::DefineTableStatement; use crate::sql::statements::DefineTableStatement;
use crate::sql::statements::DefineTokenStatement;
use crate::sql::statements::DefineUserStatement; use crate::sql::statements::DefineUserStatement;
use crate::sql::statements::LiveStatement; use crate::sql::statements::LiveStatement;
use std::collections::HashMap; use std::collections::HashMap;
@ -31,7 +30,7 @@ pub enum Entry {
// Multi definitions // Multi definitions
Azs(Arc<[DefineAnalyzerStatement]>), Azs(Arc<[DefineAnalyzerStatement]>),
Dbs(Arc<[DefineDatabaseStatement]>), Dbs(Arc<[DefineDatabaseStatement]>),
Dts(Arc<[DefineTokenStatement]>), Das(Arc<[DefineAccessStatement]>),
Dus(Arc<[DefineUserStatement]>), Dus(Arc<[DefineUserStatement]>),
Evs(Arc<[DefineEventStatement]>), Evs(Arc<[DefineEventStatement]>),
Fcs(Arc<[DefineFunctionStatement]>), Fcs(Arc<[DefineFunctionStatement]>),
@ -41,11 +40,9 @@ pub enum Entry {
Lvs(Arc<[LiveStatement]>), Lvs(Arc<[LiveStatement]>),
Mls(Arc<[DefineModelStatement]>), Mls(Arc<[DefineModelStatement]>),
Nss(Arc<[DefineNamespaceStatement]>), Nss(Arc<[DefineNamespaceStatement]>),
Nts(Arc<[DefineTokenStatement]>), Nas(Arc<[DefineAccessStatement]>),
Nus(Arc<[DefineUserStatement]>), Nus(Arc<[DefineUserStatement]>),
Pas(Arc<[DefineParamStatement]>), Pas(Arc<[DefineParamStatement]>),
Scs(Arc<[DefineScopeStatement]>),
Sts(Arc<[DefineTokenStatement]>),
Tbs(Arc<[DefineTableStatement]>), Tbs(Arc<[DefineTableStatement]>),
// Sequences // Sequences
Seq(U32), Seq(U32),

View file

@ -1311,7 +1311,7 @@ impl Datastore {
/// Evaluates a SQL [`Value`] without checking authenticating config /// Evaluates a SQL [`Value`] without checking authenticating config
/// This is used in very specific cases, where we do not need to check /// This is used in very specific cases, where we do not need to check
/// whether authentication is enabled, or guest access is disabled. /// whether authentication is enabled, or guest access is disabled.
/// For example, this is used when processing a SCOPE SIGNUP or SCOPE /// For example, this is used when processing a record access SIGNUP or
/// SIGNIN clause, which still needs to work without guest access. /// SIGNIN clause, which still needs to work without guest access.
/// ///
/// ```rust,no_run /// ```rust,no_run

View file

@ -214,7 +214,7 @@ mod test_check_lqs_and_send_notifications {
use crate::iam::{Auth, Role}; use crate::iam::{Auth, Role};
use crate::kvs::lq_v2_doc::construct_document; use crate::kvs::lq_v2_doc::construct_document;
use crate::kvs::{Datastore, LockType, TransactionType}; use crate::kvs::{Datastore, LockType, TransactionType};
use crate::sql::paths::{OBJ_PATH_AUTH, OBJ_PATH_SCOPE, OBJ_PATH_TOKEN}; use crate::sql::paths::{OBJ_PATH_ACCESS, OBJ_PATH_AUTH, OBJ_PATH_TOKEN};
use crate::sql::statements::{CreateStatement, DeleteStatement, LiveStatement}; use crate::sql::statements::{CreateStatement, DeleteStatement, LiveStatement};
use crate::sql::{Fields, Object, Strand, Table, Thing, Uuid, Value, Values}; use crate::sql::{Fields, Object, Strand, Table, Thing, Uuid, Value, Values};
@ -379,8 +379,8 @@ mod test_check_lqs_and_send_notifications {
fn a_live_query_statement() -> LiveStatement { fn a_live_query_statement() -> LiveStatement {
let mut stm = LiveStatement::new(Fields::all()); let mut stm = LiveStatement::new(Fields::all());
let mut session: BTreeMap<String, Value> = BTreeMap::new(); let mut session: BTreeMap<String, Value> = BTreeMap::new();
session.insert(OBJ_PATH_ACCESS.to_string(), Value::Strand(Strand::from("access")));
session.insert(OBJ_PATH_AUTH.to_string(), Value::Strand(Strand::from("auth"))); session.insert(OBJ_PATH_AUTH.to_string(), Value::Strand(Strand::from("auth")));
session.insert(OBJ_PATH_SCOPE.to_string(), Value::Strand(Strand::from("scope")));
session.insert(OBJ_PATH_TOKEN.to_string(), Value::Strand(Strand::from("token"))); session.insert(OBJ_PATH_TOKEN.to_string(), Value::Strand(Strand::from("token")));
let session = Value::Object(Object::from(session)); let session = Value::Object(Object::from(session));
stm.session = Some(session); stm.session = Some(session);

View file

@ -9,6 +9,7 @@ use futures::lock::Mutex;
use uuid::Uuid; use uuid::Uuid;
use sql::permission::Permissions; use sql::permission::Permissions;
use sql::statements::DefineAccessStatement;
use sql::statements::DefineAnalyzerStatement; use sql::statements::DefineAnalyzerStatement;
use sql::statements::DefineDatabaseStatement; use sql::statements::DefineDatabaseStatement;
use sql::statements::DefineEventStatement; use sql::statements::DefineEventStatement;
@ -18,9 +19,7 @@ use sql::statements::DefineIndexStatement;
use sql::statements::DefineModelStatement; use sql::statements::DefineModelStatement;
use sql::statements::DefineNamespaceStatement; use sql::statements::DefineNamespaceStatement;
use sql::statements::DefineParamStatement; use sql::statements::DefineParamStatement;
use sql::statements::DefineScopeStatement;
use sql::statements::DefineTableStatement; use sql::statements::DefineTableStatement;
use sql::statements::DefineTokenStatement;
use sql::statements::DefineUserStatement; use sql::statements::DefineUserStatement;
use sql::statements::LiveStatement; use sql::statements::LiveStatement;
@ -1442,21 +1441,24 @@ impl Transaction {
}) })
} }
/// Retrieve all namespace token definitions for a specific namespace. /// Retrieve all namespace access method definitions.
pub async fn all_ns_tokens(&mut self, ns: &str) -> Result<Arc<[DefineTokenStatement]>, Error> { pub async fn all_ns_accesses(
let key = crate::key::namespace::tk::prefix(ns); &mut self,
ns: &str,
) -> Result<Arc<[DefineAccessStatement]>, Error> {
let key = crate::key::namespace::ac::prefix(ns);
Ok(if let Some(e) = self.cache.get(&key) { Ok(if let Some(e) = self.cache.get(&key) {
if let Entry::Nts(v) = e { if let Entry::Nas(v) = e {
v v
} else { } else {
unreachable!(); unreachable!();
} }
} else { } else {
let beg = crate::key::namespace::tk::prefix(ns); let beg = crate::key::namespace::ac::prefix(ns);
let end = crate::key::namespace::tk::suffix(ns); let end = crate::key::namespace::ac::suffix(ns);
let val = self.getr(beg..end, u32::MAX).await?; let val = self.getr(beg..end, u32::MAX).await?;
let val = val.convert().into(); let val = val.convert().into();
self.cache.set(key, Entry::Nts(Arc::clone(&val))); self.cache.set(key, Entry::Nas(Arc::clone(&val)));
val val
}) })
} }
@ -1503,25 +1505,25 @@ impl Transaction {
}) })
} }
/// Retrieve all database token definitions for a specific database. /// Retrieve all database access method definitions.
pub async fn all_db_tokens( pub async fn all_db_accesses(
&mut self, &mut self,
ns: &str, ns: &str,
db: &str, db: &str,
) -> Result<Arc<[DefineTokenStatement]>, Error> { ) -> Result<Arc<[DefineAccessStatement]>, Error> {
let key = crate::key::database::tk::prefix(ns, db); let key = crate::key::database::ac::prefix(ns, db);
Ok(if let Some(e) = self.cache.get(&key) { Ok(if let Some(e) = self.cache.get(&key) {
if let Entry::Dts(v) = e { if let Entry::Das(v) = e {
v v
} else { } else {
unreachable!(); unreachable!();
} }
} else { } else {
let beg = crate::key::database::tk::prefix(ns, db); let beg = crate::key::database::ac::prefix(ns, db);
let end = crate::key::database::tk::suffix(ns, db); let end = crate::key::database::ac::suffix(ns, db);
let val = self.getr(beg..end, u32::MAX).await?; let val = self.getr(beg..end, u32::MAX).await?;
let val = val.convert().into(); let val = val.convert().into();
self.cache.set(key, Entry::Dts(Arc::clone(&val))); self.cache.set(key, Entry::Das(Arc::clone(&val)));
val val
}) })
} }
@ -1618,53 +1620,6 @@ impl Transaction {
}) })
} }
/// Retrieve all scope definitions for a specific database.
pub async fn all_sc(
&mut self,
ns: &str,
db: &str,
) -> Result<Arc<[DefineScopeStatement]>, Error> {
let key = crate::key::database::sc::prefix(ns, db);
Ok(if let Some(e) = self.cache.get(&key) {
if let Entry::Scs(v) = e {
v
} else {
unreachable!();
}
} else {
let beg = crate::key::database::sc::prefix(ns, db);
let end = crate::key::database::sc::suffix(ns, db);
let val = self.getr(beg..end, u32::MAX).await?;
let val = val.convert().into();
self.cache.set(key, Entry::Scs(Arc::clone(&val)));
val
})
}
/// Retrieve all scope token definitions for a scope.
pub async fn all_sc_tokens(
&mut self,
ns: &str,
db: &str,
sc: &str,
) -> Result<Arc<[DefineTokenStatement]>, Error> {
let key = crate::key::scope::tk::prefix(ns, db, sc);
Ok(if let Some(e) = self.cache.get(&key) {
if let Entry::Sts(v) = e {
v
} else {
unreachable!();
}
} else {
let beg = crate::key::scope::tk::prefix(ns, db, sc);
let end = crate::key::scope::tk::suffix(ns, db, sc);
let val = self.getr(beg..end, u32::MAX).await?;
let val = val.convert().into();
self.cache.set(key, Entry::Sts(Arc::clone(&val)));
val
})
}
/// Retrieve all table definitions for a specific database. /// Retrieve all table definitions for a specific database.
pub async fn all_tb( pub async fn all_tb(
&mut self, &mut self,
@ -1863,15 +1818,15 @@ impl Transaction {
Ok(val.into()) Ok(val.into())
} }
/// Retrieve a specific namespace token definition. /// Retrieve a specific namespace access method definition.
pub async fn get_ns_token( pub async fn get_ns_access(
&mut self, &mut self,
ns: &str, ns: &str,
nt: &str, ac: &str,
) -> Result<DefineTokenStatement, Error> { ) -> Result<DefineAccessStatement, Error> {
let key = crate::key::namespace::tk::new(ns, nt); let key = crate::key::namespace::ac::new(ns, ac);
let val = self.get(key).await?.ok_or(Error::NtNotFound { let val = self.get(key).await?.ok_or(Error::NaNotFound {
value: nt.to_owned(), value: ac.to_owned(),
})?; })?;
Ok(val.into()) Ok(val.into())
} }
@ -1916,16 +1871,16 @@ impl Transaction {
Ok(val.into()) Ok(val.into())
} }
/// Retrieve a specific database token definition. /// Retrieve a specific database access method definition.
pub async fn get_db_token( pub async fn get_db_access(
&mut self, &mut self,
ns: &str, ns: &str,
db: &str, db: &str,
dt: &str, ac: &str,
) -> Result<DefineTokenStatement, Error> { ) -> Result<DefineAccessStatement, Error> {
let key = crate::key::database::tk::new(ns, db, dt); let key = crate::key::database::ac::new(ns, db, ac);
let val = self.get(key).await?.ok_or(Error::DtNotFound { let val = self.get(key).await?.ok_or(Error::DaNotFound {
value: dt.to_owned(), value: ac.to_owned(),
})?; })?;
Ok(val.into()) Ok(val.into())
} }
@ -1972,35 +1927,6 @@ impl Transaction {
Ok(val.into()) Ok(val.into())
} }
/// Retrieve a specific scope definition.
pub async fn get_sc(
&mut self,
ns: &str,
db: &str,
sc: &str,
) -> Result<DefineScopeStatement, Error> {
let key = crate::key::database::sc::new(ns, db, sc);
let val = self.get(key).await?.ok_or(Error::ScNotFound {
value: sc.to_owned(),
})?;
Ok(val.into())
}
/// Retrieve a specific scope token definition.
pub async fn get_sc_token(
&mut self,
ns: &str,
db: &str,
sc: &str,
st: &str,
) -> Result<DefineTokenStatement, Error> {
let key = crate::key::scope::tk::new(ns, db, sc, st);
let val = self.get(key).await?.ok_or(Error::StNotFound {
value: st.to_owned(),
})?;
Ok(val.into())
}
/// Return the table stored at the lq address /// Return the table stored at the lq address
pub async fn get_lq( pub async fn get_lq(
&mut self, &mut self,
@ -2159,36 +2085,6 @@ impl Transaction {
} }
} }
/// Add a scope with a default configuration, only if we are in dynamic mode.
pub async fn add_sc(
&mut self,
ns: &str,
db: &str,
sc: &str,
strict: bool,
) -> Result<DefineScopeStatement, Error> {
match self.get_sc(ns, db, sc).await {
Err(Error::ScNotFound {
value,
}) => match strict {
false => {
let key = crate::key::database::sc::new(ns, db, sc);
let val = DefineScopeStatement {
name: sc.to_owned().into(),
..Default::default()
};
self.put(key.key_category(), key, &val).await?;
Ok(val)
}
true => Err(Error::ScNotFound {
value,
}),
},
Err(e) => Err(e),
Ok(v) => Ok(v),
}
}
/// Add a table with a default configuration, only if we are in dynamic mode. /// Add a table with a default configuration, only if we are in dynamic mode.
pub async fn add_tb( pub async fn add_tb(
&mut self, &mut self,
@ -2525,12 +2421,12 @@ impl Transaction {
chn.send(bytes!("")).await?; chn.send(bytes!("")).await?;
} }
} }
// Output TOKENS // Output ACCESSES
{ {
let dts = self.all_db_tokens(ns, db).await?; let dts = self.all_db_accesses(ns, db).await?;
if !dts.is_empty() { if !dts.is_empty() {
chn.send(bytes!("-- ------------------------------")).await?; chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!("-- TOKENS")).await?; chn.send(bytes!("-- ACCESSES")).await?;
chn.send(bytes!("-- ------------------------------")).await?; chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!("")).await?; chn.send(bytes!("")).await?;
for dt in dts.iter() { for dt in dts.iter() {
@ -2581,31 +2477,6 @@ impl Transaction {
chn.send(bytes!("")).await?; chn.send(bytes!("")).await?;
} }
} }
// Output SCOPES
{
let scs = self.all_sc(ns, db).await?;
if !scs.is_empty() {
chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!("-- SCOPES")).await?;
chn.send(bytes!("-- ------------------------------")).await?;
chn.send(bytes!("")).await?;
for sc in scs.iter() {
// Output SCOPE
chn.send(bytes!(format!("{sc};"))).await?;
// Output TOKENS
{
let sts = self.all_sc_tokens(ns, db, &sc.name).await?;
if !sts.is_empty() {
for st in sts.iter() {
chn.send(bytes!(format!("{st};"))).await?;
}
chn.send(bytes!("")).await?;
}
}
}
chn.send(bytes!("")).await?;
}
}
// Output TABLES // Output TABLES
{ {
let tbs = self.all_tb(ns, db).await?; let tbs = self.all_tb(ns, db).await?;

78
core/src/sql/access.rs Normal file
View file

@ -0,0 +1,78 @@
use crate::sql::{escape::escape_ident, fmt::Fmt, strand::no_nul_bytes, Id, Ident, Thing};
use revision::revisioned;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter};
use std::ops::Deref;
use std::str;
#[revisioned(revision = 1)]
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub struct Accesses(pub Vec<Access>);
impl From<Access> for Accesses {
fn from(v: Access) -> Self {
Accesses(vec![v])
}
}
impl Deref for Accesses {
type Target = Vec<Access>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for Accesses {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(&Fmt::comma_separated(&self.0), f)
}
}
#[revisioned(revision = 1)]
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
#[serde(rename = "$surrealdb::private::sql::Access")]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub struct Access(#[serde(with = "no_nul_bytes")] pub String);
impl From<String> for Access {
fn from(v: String) -> Self {
Self(v)
}
}
impl From<&str> for Access {
fn from(v: &str) -> Self {
Self::from(String::from(v))
}
}
impl From<Ident> for Access {
fn from(v: Ident) -> Self {
Self(v.0)
}
}
impl Deref for Access {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Access {
pub fn generate(&self) -> Thing {
Thing {
tb: self.0.to_owned(),
id: Id::rand(),
}
}
}
impl Display for Access {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
Display::fmt(&escape_ident(&self.0), f)
}
}

251
core/src/sql/access_type.rs Normal file
View file

@ -0,0 +1,251 @@
use crate::sql::statements::info::InfoStructure;
use crate::sql::statements::DefineAccessStatement;
use crate::sql::{escape::quote_str, Algorithm, Duration};
use revision::revisioned;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::fmt::Display;
use super::{Object, Value};
/// The type of access methods available
#[revisioned(revision = 1)]
#[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),
}
impl Default for AccessType {
fn default() -> Self {
// Access type defaults to the most specific
Self::Record(RecordAccess {
..Default::default()
})
}
}
#[revisioned(revision = 1)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct JwtAccess {
// Verify is required
pub verify: JwtAccessVerify,
// Issue is optional
// It is possible to only verify externally issued tokens
pub issue: Option<JwtAccessIssue>,
}
impl Default for JwtAccess {
fn default() -> Self {
// Defaults to HS512 with a randomly generated key
let alg = Algorithm::Hs512;
let key = DefineAccessStatement::random_key();
// By default the access method can verify and issue tokens
Self {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg,
key: key.clone(),
}),
issue: Some(JwtAccessIssue {
alg,
key,
// Defaults to tokens lasting for one hour
duration: Some(Duration::from_hours(1)),
}),
}
}
}
#[revisioned(revision = 1)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct JwtAccessIssue {
pub alg: Algorithm,
pub key: String,
pub duration: Option<Duration>,
}
impl Default for JwtAccessIssue {
fn default() -> Self {
Self {
// Defaults to HS512
alg: Algorithm::Hs512,
// Avoid defaulting to empty key
key: DefineAccessStatement::random_key(),
// Defaults to tokens lasting for one hour
duration: Some(Duration::from_hours(1)),
}
}
}
#[revisioned(revision = 1)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub enum JwtAccessVerify {
Key(JwtAccessVerifyKey),
Jwks(JwtAccessVerifyJwks),
}
impl Default for JwtAccessVerify {
fn default() -> Self {
Self::Key(JwtAccessVerifyKey {
..Default::default()
})
}
}
#[revisioned(revision = 1)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct JwtAccessVerifyKey {
pub alg: Algorithm,
pub key: String,
}
impl Default for JwtAccessVerifyKey {
fn default() -> Self {
Self {
// Defaults to HS512
alg: Algorithm::Hs512,
// Avoid defaulting to empty key
key: DefineAccessStatement::random_key(),
}
}
}
#[revisioned(revision = 1)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct JwtAccessVerifyJwks {
pub url: String,
}
#[revisioned(revision = 1)]
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct RecordAccess {
pub duration: Option<Duration>,
pub signup: Option<Value>,
pub signin: Option<Value>,
pub jwt: JwtAccess,
}
impl Default for RecordAccess {
fn default() -> Self {
Self {
// Defaults to sessions lasting one hour
duration: Some(Duration::from_hours(1)),
signup: None,
signin: None,
jwt: JwtAccess {
..Default::default()
},
}
}
}
impl Display for AccessType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AccessType::Jwt(ac) => {
f.write_str(" JWT")?;
match &ac.verify {
JwtAccessVerify::Key(ref v) => {
write!(f, " ALGORITHM {} KEY {}", v.alg, quote_str(&v.key))?
}
JwtAccessVerify::Jwks(ref v) => write!(f, " JWKS {}", quote_str(&v.url))?,
}
}
AccessType::Record(ac) => {
f.write_str(" RECORD")?;
if let Some(ref v) = ac.duration {
write!(f, " DURATION {v}")?
}
if let Some(ref v) = ac.signup {
write!(f, " SIGNUP {v}")?
}
if let Some(ref v) = ac.signin {
write!(f, " SIGNIN {v}")?
}
}
}
Ok(())
}
}
impl InfoStructure for AccessType {
fn structure(self) -> Value {
let mut acc = Object::default();
match self {
AccessType::Jwt(ac) => {
acc.insert("kind".to_string(), "JWT".into());
acc.insert("jwt".to_string(), ac.structure());
}
AccessType::Record(ac) => {
acc.insert("kind".to_string(), "RECORD".into());
if let Some(signup) = ac.signup {
acc.insert("signup".to_string(), signup.structure());
}
if let Some(signin) = ac.signin {
acc.insert("signin".to_string(), signin.structure());
}
if let Some(duration) = ac.duration {
acc.insert("duration".to_string(), duration.into());
}
acc.insert("jwt".to_string(), ac.jwt.structure());
}
};
Value::Object(acc)
}
}
impl Display for JwtAccess {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match &self.verify {
JwtAccessVerify::Key(ref v) => {
write!(f, "ALGORITHM {} KEY {}", v.alg, quote_str(&v.key))?;
}
JwtAccessVerify::Jwks(ref v) => {
write!(f, "JWKS {}", quote_str(&v.url),)?;
}
}
if let Some(iss) = &self.issue {
write!(f, " WITH ISSUER KEY {}", quote_str(&iss.key))?;
if let Some(ref v) = iss.duration {
write!(f, " DURATION {v}")?
}
}
Ok(())
}
}
impl InfoStructure for JwtAccess {
fn structure(self) -> Value {
let mut acc = Object::default();
match self.verify {
JwtAccessVerify::Key(v) => {
acc.insert("alg".to_string(), v.alg.structure());
acc.insert("key".to_string(), v.key.into());
}
JwtAccessVerify::Jwks(v) => {
acc.insert("jwks".to_string(), v.url.into());
}
}
if let Some(v) = self.issue {
let mut iss = Object::default();
iss.insert("alg".to_string(), v.alg.structure());
iss.insert("key".to_string(), v.key.into());
if let Some(t) = v.duration {
iss.insert("duration".to_string(), t.into());
}
acc.insert("issuer".to_string(), iss.to_string().into());
}
Value::Object(acc)
}
}

View file

@ -22,7 +22,33 @@ pub enum Algorithm {
Rs256, Rs256,
Rs384, Rs384,
Rs512, Rs512,
Jwks, // Not an argorithm. }
impl Algorithm {
// Does the algorithm us the same key for signing and verification?
pub(crate) fn is_symmetric(self) -> bool {
matches!(self, Algorithm::Hs256 | Algorithm::Hs384 | Algorithm::Hs512)
}
}
impl From<Algorithm> for jsonwebtoken::Algorithm {
fn from(val: Algorithm) -> Self {
match val {
Algorithm::Hs256 => jsonwebtoken::Algorithm::HS256,
Algorithm::Hs384 => jsonwebtoken::Algorithm::HS384,
Algorithm::Hs512 => jsonwebtoken::Algorithm::HS512,
Algorithm::EdDSA => jsonwebtoken::Algorithm::EdDSA,
Algorithm::Es256 => jsonwebtoken::Algorithm::ES256,
Algorithm::Es384 => jsonwebtoken::Algorithm::ES384,
Algorithm::Es512 => jsonwebtoken::Algorithm::ES384,
Algorithm::Ps256 => jsonwebtoken::Algorithm::PS256,
Algorithm::Ps384 => jsonwebtoken::Algorithm::PS384,
Algorithm::Ps512 => jsonwebtoken::Algorithm::PS512,
Algorithm::Rs256 => jsonwebtoken::Algorithm::RS256,
Algorithm::Rs384 => jsonwebtoken::Algorithm::RS384,
Algorithm::Rs512 => jsonwebtoken::Algorithm::RS512,
}
}
} }
impl Default for Algorithm { impl Default for Algorithm {
@ -47,7 +73,6 @@ impl fmt::Display for Algorithm {
Self::Rs256 => "RS256", Self::Rs256 => "RS256",
Self::Rs384 => "RS384", Self::Rs384 => "RS384",
Self::Rs512 => "RS512", Self::Rs512 => "RS512",
Self::Jwks => "JWKS", // Not an algorithm.
}) })
} }
} }

View file

@ -12,6 +12,7 @@ pub enum Base {
Root, Root,
Ns, Ns,
Db, Db,
// TODO(gguillemas): This variant is kept in 2.0.0 for backward compatibility. Drop in 3.0.0.
Sc(Ident), Sc(Ident),
} }
@ -26,6 +27,7 @@ impl fmt::Display for Base {
match self { match self {
Self::Ns => f.write_str("NAMESPACE"), Self::Ns => f.write_str("NAMESPACE"),
Self::Db => f.write_str("DATABASE"), Self::Db => f.write_str("DATABASE"),
// TODO(gguillemas): This variant is kept in 2.0.0 for backward compatibility. Drop in 3.0.0.
Self::Sc(sc) => write!(f, "SCOPE {sc}"), Self::Sc(sc) => write!(f, "SCOPE {sc}"),
Self::Root => f.write_str("ROOT"), Self::Root => f.write_str("ROOT"),
} }

View file

@ -1,5 +1,7 @@
//! The full type definitions for the SurrealQL query language //! The full type definitions for the SurrealQL query language
pub(crate) mod access;
pub(crate) mod access_type;
pub(crate) mod algorithm; pub(crate) mod algorithm;
#[cfg(feature = "arbitrary")] #[cfg(feature = "arbitrary")]
pub(crate) mod arbitrary; pub(crate) mod arbitrary;
@ -74,6 +76,9 @@ pub mod index;
pub mod serde; pub mod serde;
pub mod statements; pub mod statements;
pub use self::access::Access;
pub use self::access::Accesses;
pub use self::access_type::{AccessType, JwtAccess, RecordAccess};
pub use self::algorithm::Algorithm; pub use self::algorithm::Algorithm;
pub use self::array::Array; pub use self::array::Array;
pub use self::base::Base; pub use self::base::Base;

View file

@ -1,9 +1,10 @@
use crate::sql::part::Part; use crate::sql::part::Part;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
pub const OBJ_PATH_AUTH: &str = "sd"; pub const OBJ_PATH_ACCESS: &str = "ac";
pub const OBJ_PATH_SCOPE: &str = "sc"; pub const OBJ_PATH_AUTH: &str = "rd";
pub const OBJ_PATH_TOKEN: &str = "tk"; pub const OBJ_PATH_TOKEN: &str = "tk";
pub static ID: Lazy<[Part; 1]> = Lazy::new(|| [Part::from("id")]); pub static ID: Lazy<[Part; 1]> = Lazy::new(|| [Part::from("id")]);
pub static IP: Lazy<[Part; 1]> = Lazy::new(|| [Part::from("ip")]); pub static IP: Lazy<[Part; 1]> = Lazy::new(|| [Part::from("ip")]);
@ -12,9 +13,9 @@ pub static NS: Lazy<[Part; 1]> = Lazy::new(|| [Part::from("ns")]);
pub static DB: Lazy<[Part; 1]> = Lazy::new(|| [Part::from("db")]); pub static DB: Lazy<[Part; 1]> = Lazy::new(|| [Part::from("db")]);
pub static SC: Lazy<[Part; 1]> = Lazy::new(|| [Part::from(OBJ_PATH_SCOPE)]); pub static AC: Lazy<[Part; 1]> = Lazy::new(|| [Part::from(OBJ_PATH_ACCESS)]);
pub static SD: Lazy<[Part; 1]> = Lazy::new(|| [Part::from(OBJ_PATH_AUTH)]); pub static RD: Lazy<[Part; 1]> = Lazy::new(|| [Part::from(OBJ_PATH_AUTH)]);
pub static OR: Lazy<[Part; 1]> = Lazy::new(|| [Part::from("or")]); pub static OR: Lazy<[Part; 1]> = Lazy::new(|| [Part::from("or")]);

View file

@ -4,27 +4,37 @@ use crate::doc::CursorDoc;
use crate::err::Error; use crate::err::Error;
use crate::iam::{Action, ResourceKind}; use crate::iam::{Action, ResourceKind};
use crate::sql::statements::info::InfoStructure; use crate::sql::statements::info::InfoStructure;
use crate::sql::{escape::quote_str, Algorithm, Base, Ident, Object, Strand, Value}; use crate::sql::{AccessType, Base, Ident, Object, Strand, Value};
use derive::Store; use derive::Store;
use rand::distributions::Alphanumeric;
use rand::Rng;
use revision::revisioned; use revision::revisioned;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::{self, Display}; use std::fmt::{self, Display};
#[revisioned(revision = 2)] #[revisioned(revision = 2)]
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] #[derive(Clone, Default, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive] #[non_exhaustive]
pub struct DefineTokenStatement { pub struct DefineAccessStatement {
pub name: Ident, pub name: Ident,
pub base: Base, pub base: Base,
pub kind: Algorithm, pub kind: AccessType,
pub code: String,
pub comment: Option<Strand>, pub comment: Option<Strand>,
#[revision(start = 2)] #[revision(start = 2)]
pub if_not_exists: bool, pub if_not_exists: bool,
} }
impl DefineTokenStatement { impl DefineAccessStatement {
/// Generate a random key to be used to sign session tokens
/// This key will be used to sign tokens issued with this access method
/// This value is used by default in every access method other than JWT
pub(crate) fn random_key() -> String {
rand::thread_rng().sample_iter(&Alphanumeric).take(128).map(char::from).collect::<String>()
}
}
impl DefineAccessStatement {
/// Process this type returning a computed simple Value /// Process this type returning a computed simple Value
pub(crate) async fn compute( pub(crate) async fn compute(
&self, &self,
@ -41,18 +51,18 @@ impl DefineTokenStatement {
let mut run = txn.lock().await; let mut run = txn.lock().await;
// Clear the cache // Clear the cache
run.clear_cache(); run.clear_cache();
// Check if token already exists // Check if access method already exists
if self.if_not_exists && run.get_ns_token(opt.ns(), &self.name).await.is_ok() { if self.if_not_exists && run.get_ns_access(opt.ns(), &self.name).await.is_ok() {
return Err(Error::NtAlreadyExists { return Err(Error::AccessNsAlreadyExists {
value: self.name.to_string(), value: self.name.to_string(),
}); });
} }
// Process the statement // Process the statement
let key = crate::key::namespace::tk::new(opt.ns(), &self.name); let key = crate::key::namespace::ac::new(opt.ns(), &self.name);
run.add_ns(opt.ns(), opt.strict).await?; run.add_ns(opt.ns(), opt.strict).await?;
run.set( run.set(
key, key,
DefineTokenStatement { DefineAccessStatement {
if_not_exists: false, if_not_exists: false,
..self.clone() ..self.clone()
}, },
@ -66,50 +76,21 @@ impl DefineTokenStatement {
let mut run = txn.lock().await; let mut run = txn.lock().await;
// Clear the cache // Clear the cache
run.clear_cache(); run.clear_cache();
// Check if token already exists // Check if access method already exists
if self.if_not_exists if self.if_not_exists
&& run.get_db_token(opt.ns(), opt.db(), &self.name).await.is_ok() && run.get_db_access(opt.ns(), opt.db(), &self.name).await.is_ok()
{ {
return Err(Error::DtAlreadyExists { return Err(Error::AccessDbAlreadyExists {
value: self.name.to_string(), value: self.name.to_string(),
}); });
} }
// Process the statement // Process the statement
let key = crate::key::database::tk::new(opt.ns(), opt.db(), &self.name); let key = crate::key::database::ac::new(opt.ns(), opt.db(), &self.name);
run.add_ns(opt.ns(), opt.strict).await?; run.add_ns(opt.ns(), opt.strict).await?;
run.add_db(opt.ns(), opt.db(), opt.strict).await?; run.add_db(opt.ns(), opt.db(), opt.strict).await?;
run.set( run.set(
key, key,
DefineTokenStatement { DefineAccessStatement {
if_not_exists: false,
..self.clone()
},
)
.await?;
// Ok all good
Ok(Value::None)
}
Base::Sc(sc) => {
// Claim transaction
let mut run = txn.lock().await;
// Clear the cache
run.clear_cache();
// Check if token already exists
if self.if_not_exists
&& run.get_sc_token(opt.ns(), opt.db(), sc, &self.name).await.is_ok()
{
return Err(Error::StAlreadyExists {
value: self.name.to_string(),
});
}
// Process the statement
let key = crate::key::scope::tk::new(opt.ns(), opt.db(), sc, &self.name);
run.add_ns(opt.ns(), opt.strict).await?;
run.add_db(opt.ns(), opt.db(), opt.strict).await?;
run.add_sc(opt.ns(), opt.db(), sc, opt.strict).await?;
run.set(
key,
DefineTokenStatement {
if_not_exists: false, if_not_exists: false,
..self.clone() ..self.clone()
}, },
@ -124,20 +105,31 @@ impl DefineTokenStatement {
} }
} }
impl Display for DefineTokenStatement { impl Display for DefineAccessStatement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DEFINE TOKEN",)?; write!(f, "DEFINE ACCESS",)?;
if self.if_not_exists { if self.if_not_exists {
write!(f, " IF NOT EXISTS")? write!(f, " IF NOT EXISTS")?
} }
write!( write!(f, " {} ON {}", self.name, self.base)?;
f, match &self.kind {
" {} ON {} TYPE {} VALUE {}", AccessType::Jwt(ac) => {
self.name, write!(f, " TYPE JWT {}", ac)?;
self.base, }
self.kind, AccessType::Record(ac) => {
quote_str(&self.code) write!(f, " TYPE RECORD")?;
)?; if let Some(ref v) = ac.duration {
write!(f, " DURATION {v}")?
}
if let Some(ref v) = ac.signup {
write!(f, " SIGNUP {v}")?
}
if let Some(ref v) = ac.signin {
write!(f, " SIGNIN {v}")?
}
write!(f, " WITH JWT {}", ac.jwt)?;
}
}
if let Some(ref v) = self.comment { if let Some(ref v) = self.comment {
write!(f, " COMMENT {v}")? write!(f, " COMMENT {v}")?
} }
@ -145,13 +137,12 @@ impl Display for DefineTokenStatement {
} }
} }
impl InfoStructure for DefineTokenStatement { impl InfoStructure for DefineAccessStatement {
fn structure(self) -> Value { fn structure(self) -> Value {
let Self { let Self {
name, name,
base, base,
kind, kind,
code,
comment, comment,
.. ..
} = self; } = self;
@ -163,8 +154,6 @@ impl InfoStructure for DefineTokenStatement {
acc.insert("kind".to_string(), kind.structure()); acc.insert("kind".to_string(), kind.structure());
acc.insert("code".to_string(), code.into());
if let Some(comment) = comment { if let Some(comment) = comment {
acc.insert("comment".to_string(), comment.into()); acc.insert("comment".to_string(), comment.into());
} }

View file

@ -1,3 +1,4 @@
mod access;
mod analyzer; mod analyzer;
mod database; mod database;
mod event; mod event;
@ -7,11 +8,10 @@ mod index;
mod model; mod model;
mod namespace; mod namespace;
mod param; mod param;
mod scope;
mod table; mod table;
mod token;
mod user; mod user;
pub use access::DefineAccessStatement;
pub use analyzer::DefineAnalyzerStatement; pub use analyzer::DefineAnalyzerStatement;
pub use database::DefineDatabaseStatement; pub use database::DefineDatabaseStatement;
pub use event::DefineEventStatement; pub use event::DefineEventStatement;
@ -21,9 +21,7 @@ pub use index::DefineIndexStatement;
pub use model::DefineModelStatement; pub use model::DefineModelStatement;
pub use namespace::DefineNamespaceStatement; pub use namespace::DefineNamespaceStatement;
pub use param::DefineParamStatement; pub use param::DefineParamStatement;
pub use scope::DefineScopeStatement;
pub use table::DefineTableStatement; pub use table::DefineTableStatement;
pub use token::DefineTokenStatement;
pub use user::DefineUserStatement; pub use user::DefineUserStatement;
use crate::ctx::Context; use crate::ctx::Context;
@ -47,8 +45,6 @@ pub enum DefineStatement {
Database(DefineDatabaseStatement), Database(DefineDatabaseStatement),
Function(DefineFunctionStatement), Function(DefineFunctionStatement),
Analyzer(DefineAnalyzerStatement), Analyzer(DefineAnalyzerStatement),
Token(DefineTokenStatement),
Scope(DefineScopeStatement),
Param(DefineParamStatement), Param(DefineParamStatement),
Table(DefineTableStatement), Table(DefineTableStatement),
Event(DefineEventStatement), Event(DefineEventStatement),
@ -56,6 +52,7 @@ pub enum DefineStatement {
Index(DefineIndexStatement), Index(DefineIndexStatement),
User(DefineUserStatement), User(DefineUserStatement),
Model(DefineModelStatement), Model(DefineModelStatement),
Access(DefineAccessStatement),
} }
impl DefineStatement { impl DefineStatement {
@ -76,8 +73,6 @@ impl DefineStatement {
Self::Namespace(ref v) => v.compute(ctx, opt, txn, doc).await, Self::Namespace(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Database(ref v) => v.compute(ctx, opt, txn, doc).await, Self::Database(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Function(ref v) => v.compute(ctx, opt, txn, doc).await, Self::Function(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Token(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Scope(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Param(ref v) => v.compute(stk, ctx, opt, txn, doc).await, Self::Param(ref v) => v.compute(stk, ctx, opt, txn, doc).await,
Self::Table(ref v) => v.compute(stk, ctx, opt, txn, doc).await, Self::Table(ref v) => v.compute(stk, ctx, opt, txn, doc).await,
Self::Event(ref v) => v.compute(ctx, opt, txn, doc).await, Self::Event(ref v) => v.compute(ctx, opt, txn, doc).await,
@ -86,6 +81,7 @@ impl DefineStatement {
Self::Analyzer(ref v) => v.compute(ctx, opt, txn, doc).await, Self::Analyzer(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::User(ref v) => v.compute(ctx, opt, txn, doc).await, Self::User(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Model(ref v) => v.compute(ctx, opt, txn, doc).await, Self::Model(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Access(ref v) => v.compute(ctx, opt, txn, doc).await,
} }
} }
} }
@ -97,8 +93,6 @@ impl Display for DefineStatement {
Self::Database(v) => Display::fmt(v, f), Self::Database(v) => Display::fmt(v, f),
Self::Function(v) => Display::fmt(v, f), Self::Function(v) => Display::fmt(v, f),
Self::User(v) => Display::fmt(v, f), Self::User(v) => Display::fmt(v, f),
Self::Token(v) => Display::fmt(v, f),
Self::Scope(v) => Display::fmt(v, f),
Self::Param(v) => Display::fmt(v, f), Self::Param(v) => Display::fmt(v, f),
Self::Table(v) => Display::fmt(v, f), Self::Table(v) => Display::fmt(v, f),
Self::Event(v) => Display::fmt(v, f), Self::Event(v) => Display::fmt(v, f),
@ -106,6 +100,7 @@ impl Display for DefineStatement {
Self::Index(v) => Display::fmt(v, f), Self::Index(v) => Display::fmt(v, f),
Self::Analyzer(v) => Display::fmt(v, f), Self::Analyzer(v) => Display::fmt(v, f),
Self::Model(v) => Display::fmt(v, f), Self::Model(v) => Display::fmt(v, f),
Self::Access(v) => Display::fmt(v, f),
} }
} }
} }

View file

@ -1,130 +0,0 @@
use crate::ctx::Context;
use crate::dbs::{Options, Transaction};
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::iam::{Action, ResourceKind};
use crate::sql::statements::info::InfoStructure;
use crate::sql::{Base, Duration, Ident, Object, Strand, Value};
use derive::Store;
use rand::distributions::Alphanumeric;
use rand::Rng;
use revision::revisioned;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
#[revisioned(revision = 2)]
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub struct DefineScopeStatement {
pub name: Ident,
pub code: String,
pub session: Option<Duration>,
pub signup: Option<Value>,
pub signin: Option<Value>,
pub comment: Option<Strand>,
#[revision(start = 2)]
pub if_not_exists: bool,
}
impl DefineScopeStatement {
pub(crate) fn random_code() -> String {
rand::thread_rng().sample_iter(&Alphanumeric).take(128).map(char::from).collect::<String>()
}
}
impl DefineScopeStatement {
/// Process this type returning a computed simple Value
pub(crate) async fn compute(
&self,
_ctx: &Context<'_>,
opt: &Options,
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Allowed to run?
opt.is_allowed(Action::Edit, ResourceKind::Scope, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Clear the cache
run.clear_cache();
// Check if scope already exists
if self.if_not_exists && run.get_sc(opt.ns(), opt.db(), &self.name).await.is_ok() {
return Err(Error::ScAlreadyExists {
value: self.name.to_string(),
});
}
// Process the statement
let key = crate::key::database::sc::new(opt.ns(), opt.db(), &self.name);
run.add_ns(opt.ns(), opt.strict).await?;
run.add_db(opt.ns(), opt.db(), opt.strict).await?;
run.set(
key,
DefineScopeStatement {
// Don't persist the "IF NOT EXISTS" clause to schema
if_not_exists: false,
..self.clone()
},
)
.await?;
// Ok all good
Ok(Value::None)
}
}
impl Display for DefineScopeStatement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DEFINE SCOPE")?;
if self.if_not_exists {
write!(f, " IF NOT EXISTS")?
}
write!(f, " {}", self.name)?;
if let Some(ref v) = self.session {
write!(f, " SESSION {v}")?
}
if let Some(ref v) = self.signup {
write!(f, " SIGNUP {v}")?
}
if let Some(ref v) = self.signin {
write!(f, " SIGNIN {v}")?
}
if let Some(ref v) = self.comment {
write!(f, " COMMENT {v}")?
}
Ok(())
}
}
impl InfoStructure for DefineScopeStatement {
fn structure(self) -> Value {
let Self {
name,
signup,
signin,
comment,
session,
..
} = self;
let mut acc = Object::default();
acc.insert("name".to_string(), name.structure());
if let Some(signup) = signup {
acc.insert("signup".to_string(), signup.structure());
}
if let Some(signin) = signin {
acc.insert("signin".to_string(), signin.structure());
}
if let Some(comment) = comment {
acc.insert("comment".to_string(), comment.into());
}
if let Some(duration) = session {
acc.insert("duration".to_string(), duration.into());
}
Value::Object(acc)
}
}

View file

@ -27,10 +27,6 @@ pub enum InfoStatement {
Db, Db,
#[revision(start = 2)] #[revision(start = 2)]
Db(bool), Db(bool),
#[revision(end = 2, convert_fn = "sc_migrate")]
Sc(Ident),
#[revision(start = 2)]
Sc(Ident, bool),
#[revision(end = 2, convert_fn = "tb_migrate")] #[revision(end = 2, convert_fn = "tb_migrate")]
Tb(Ident), Tb(Ident),
#[revision(start = 2)] #[revision(start = 2)]
@ -54,10 +50,6 @@ impl InfoStatement {
Ok(Self::Db(false)) Ok(Self::Db(false))
} }
fn sc_migrate(_revision: u16, i: (Ident,)) -> Result<Self, revision::Error> {
Ok(Self::Sc(i.0, false))
}
fn tb_migrate(_revision: u16, n: (Ident,)) -> Result<Self, revision::Error> { fn tb_migrate(_revision: u16, n: (Ident,)) -> Result<Self, revision::Error> {
Ok(Self::Tb(n.0, false)) Ok(Self::Tb(n.0, false))
} }
@ -122,12 +114,12 @@ impl InfoStatement {
tmp.insert(v.name.to_string(), v.to_string().into()); tmp.insert(v.name.to_string(), v.to_string().into());
} }
res.insert("users".to_owned(), tmp.into()); res.insert("users".to_owned(), tmp.into());
// Process the tokens // Process the accesses
let mut tmp = Object::default(); let mut tmp = Object::default();
for v in run.all_ns_tokens(opt.ns()).await?.iter() { for v in run.all_ns_accesses(opt.ns()).await?.iter() {
tmp.insert(v.name.to_string(), v.to_string().into()); tmp.insert(v.name.to_string(), v.to_string().into());
} }
res.insert("tokens".to_owned(), tmp.into()); res.insert("accesses".to_owned(), tmp.into());
// Ok all good // Ok all good
Value::from(res).ok() Value::from(res).ok()
} }
@ -144,12 +136,6 @@ impl InfoStatement {
tmp.insert(v.name.to_string(), v.to_string().into()); tmp.insert(v.name.to_string(), v.to_string().into());
} }
res.insert("users".to_owned(), tmp.into()); res.insert("users".to_owned(), tmp.into());
// Process the tokens
let mut tmp = Object::default();
for v in run.all_db_tokens(opt.ns(), opt.db()).await?.iter() {
tmp.insert(v.name.to_string(), v.to_string().into());
}
res.insert("tokens".to_owned(), tmp.into());
// Process the functions // Process the functions
let mut tmp = Object::default(); let mut tmp = Object::default();
for v in run.all_db_functions(opt.ns(), opt.db()).await?.iter() { for v in run.all_db_functions(opt.ns(), opt.db()).await?.iter() {
@ -168,12 +154,12 @@ impl InfoStatement {
tmp.insert(v.name.to_string(), v.to_string().into()); tmp.insert(v.name.to_string(), v.to_string().into());
} }
res.insert("params".to_owned(), tmp.into()); res.insert("params".to_owned(), tmp.into());
// Process the scopes // Process the accesses
let mut tmp = Object::default(); let mut tmp = Object::default();
for v in run.all_sc(opt.ns(), opt.db()).await?.iter() { for v in run.all_db_accesses(opt.ns(), opt.db()).await?.iter() {
tmp.insert(v.name.to_string(), v.to_string().into()); tmp.insert(v.name.to_string(), v.to_string().into());
} }
res.insert("scopes".to_owned(), tmp.into()); res.insert("accesses".to_owned(), tmp.into());
// Process the tables // Process the tables
let mut tmp = Object::default(); let mut tmp = Object::default();
for v in run.all_tb(opt.ns(), opt.db()).await?.iter() { for v in run.all_tb(opt.ns(), opt.db()).await?.iter() {
@ -189,22 +175,6 @@ impl InfoStatement {
// Ok all good // Ok all good
Value::from(res).ok() Value::from(res).ok()
} }
InfoStatement::Sc(sc, false) => {
// Allowed to run?
opt.is_allowed(Action::View, ResourceKind::Any, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Create the result set
let mut res = Object::default();
// Process the tokens
let mut tmp = Object::default();
for v in run.all_sc_tokens(opt.ns(), opt.db(), sc).await?.iter() {
tmp.insert(v.name.to_string(), v.to_string().into());
}
res.insert("tokens".to_owned(), tmp.into());
// Ok all good
Value::from(res).ok()
}
InfoStatement::Tb(tb, false) => { InfoStatement::Tb(tb, false) => {
// Allowed to run? // Allowed to run?
opt.is_allowed(Action::View, ResourceKind::Any, &Base::Db)?; opt.is_allowed(Action::View, ResourceKind::Any, &Base::Db)?;
@ -287,8 +257,11 @@ impl InfoStatement {
res.insert("databases".to_owned(), process_arr(run.all_db(opt.ns()).await?)); res.insert("databases".to_owned(), process_arr(run.all_db(opt.ns()).await?));
// Process the users // Process the users
res.insert("users".to_owned(), process_arr(run.all_ns_users(opt.ns()).await?)); res.insert("users".to_owned(), process_arr(run.all_ns_users(opt.ns()).await?));
// Process the tokens // Process the accesses
res.insert("tokens".to_owned(), process_arr(run.all_ns_tokens(opt.ns()).await?)); res.insert(
"accesses".to_owned(),
process_arr(run.all_ns_accesses(opt.ns()).await?),
);
// Ok all good // Ok all good
Value::from(res).ok() Value::from(res).ok()
} }
@ -304,10 +277,10 @@ impl InfoStatement {
"users".to_owned(), "users".to_owned(),
process_arr(run.all_db_users(opt.ns(), opt.db()).await?), process_arr(run.all_db_users(opt.ns(), opt.db()).await?),
); );
// Process the tokens // Process the accesses
res.insert( res.insert(
"tokens".to_owned(), "accesses".to_owned(),
process_arr(run.all_db_tokens(opt.ns(), opt.db()).await?), process_arr(run.all_db_accesses(opt.ns(), opt.db()).await?),
); );
// Process the functions // Process the functions
res.insert( res.insert(
@ -324,8 +297,11 @@ impl InfoStatement {
"params".to_owned(), "params".to_owned(),
process_arr(run.all_db_params(opt.ns(), opt.db()).await?), process_arr(run.all_db_params(opt.ns(), opt.db()).await?),
); );
// Process the scopes // Process the accesses
res.insert("scopes".to_owned(), process_arr(run.all_sc(opt.ns(), opt.db()).await?)); res.insert(
"accesses".to_owned(),
process_arr(run.all_db_accesses(opt.ns(), opt.db()).await?),
);
// Process the tables // Process the tables
res.insert("tables".to_owned(), process_arr(run.all_tb(opt.ns(), opt.db()).await?)); res.insert("tables".to_owned(), process_arr(run.all_tb(opt.ns(), opt.db()).await?));
// Process the analyzers // Process the analyzers
@ -336,30 +312,6 @@ impl InfoStatement {
// Ok all good // Ok all good
Value::from(res).ok() Value::from(res).ok()
} }
InfoStatement::Sc(sc, true) => {
// Allowed to run?
opt.is_allowed(Action::View, ResourceKind::Any, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Create the result set
let mut res = Object::default();
// Process the tokens
res.insert(
"tokens".to_owned(),
process_arr(run.all_sc_tokens(opt.ns(), opt.db(), sc).await?),
);
let def = run.get_sc(opt.ns(), opt.db(), sc).await?;
let Value::Object(o) = def.structure() else {
return Err(Error::Thrown(
"InfoStructure should return Value::Object".to_string(),
));
};
res.extend(o);
// Ok all good
Value::from(res).ok()
}
InfoStatement::Tb(tb, true) => { InfoStatement::Tb(tb, true) => {
// Allowed to run? // Allowed to run?
opt.is_allowed(Action::View, ResourceKind::Any, &Base::Db)?; opt.is_allowed(Action::View, ResourceKind::Any, &Base::Db)?;
@ -425,8 +377,6 @@ impl fmt::Display for InfoStatement {
Self::Ns(true) => f.write_str("INFO FOR NAMESPACE STRUCTURE"), Self::Ns(true) => f.write_str("INFO FOR NAMESPACE STRUCTURE"),
Self::Db(false) => f.write_str("INFO FOR DATABASE"), Self::Db(false) => f.write_str("INFO FOR DATABASE"),
Self::Db(true) => f.write_str("INFO FOR DATABASE STRUCTURE"), Self::Db(true) => f.write_str("INFO FOR DATABASE STRUCTURE"),
Self::Sc(ref s, false) => write!(f, "INFO FOR SCOPE {s}"),
Self::Sc(ref s, true) => write!(f, "INFO FOR SCOPE {s} STRUCTURE"),
Self::Tb(ref t, false) => write!(f, "INFO FOR TABLE {t}"), Self::Tb(ref t, false) => write!(f, "INFO FOR TABLE {t}"),
Self::Tb(ref t, true) => write!(f, "INFO FOR TABLE {t} STRUCTURE"), Self::Tb(ref t, true) => write!(f, "INFO FOR TABLE {t} STRUCTURE"),
Self::User(ref u, ref b, false) => match b { Self::User(ref u, ref b, false) => match b {
@ -453,7 +403,6 @@ impl InfoStatement {
InfoStatement::Root(_) => InfoStatement::Root(true), InfoStatement::Root(_) => InfoStatement::Root(true),
InfoStatement::Ns(_) => InfoStatement::Ns(true), InfoStatement::Ns(_) => InfoStatement::Ns(true),
InfoStatement::Db(_) => InfoStatement::Db(true), InfoStatement::Db(_) => InfoStatement::Db(true),
InfoStatement::Sc(s, _) => InfoStatement::Sc(s, true),
InfoStatement::Tb(t, _) => InfoStatement::Tb(t, true), InfoStatement::Tb(t, _) => InfoStatement::Tb(t, true),
InfoStatement::User(u, b, _) => InfoStatement::User(u, b, true), InfoStatement::User(u, b, _) => InfoStatement::User(u, b, true),
} }

View file

@ -52,15 +52,15 @@ pub use self::throw::ThrowStatement;
pub use self::update::UpdateStatement; pub use self::update::UpdateStatement;
pub use self::define::{ pub use self::define::{
DefineAnalyzerStatement, DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement, DefineAccessStatement, DefineAnalyzerStatement, DefineDatabaseStatement, DefineEventStatement,
DefineFunctionStatement, DefineIndexStatement, DefineModelStatement, DefineNamespaceStatement, DefineFieldStatement, DefineFunctionStatement, DefineIndexStatement, DefineModelStatement,
DefineParamStatement, DefineScopeStatement, DefineStatement, DefineTableStatement, DefineNamespaceStatement, DefineParamStatement, DefineStatement, DefineTableStatement,
DefineTokenStatement, DefineUserStatement, DefineUserStatement,
}; };
pub use self::remove::{ pub use self::remove::{
RemoveAnalyzerStatement, RemoveDatabaseStatement, RemoveEventStatement, RemoveFieldStatement, RemoveAccessStatement, RemoveAnalyzerStatement, RemoveDatabaseStatement, RemoveEventStatement,
RemoveFunctionStatement, RemoveIndexStatement, RemoveModelStatement, RemoveNamespaceStatement, RemoveFieldStatement, RemoveFunctionStatement, RemoveIndexStatement, RemoveModelStatement,
RemoveParamStatement, RemoveScopeStatement, RemoveStatement, RemoveTableStatement, RemoveNamespaceStatement, RemoveParamStatement, RemoveStatement, RemoveTableStatement,
RemoveTokenStatement, RemoveUserStatement, RemoveUserStatement,
}; };

View file

@ -12,14 +12,14 @@ use std::fmt::{self, Display, Formatter};
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] #[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive] #[non_exhaustive]
pub struct RemoveTokenStatement { pub struct RemoveAccessStatement {
pub name: Ident, pub name: Ident,
pub base: Base, pub base: Base,
#[revision(start = 2)] #[revision(start = 2)]
pub if_exists: bool, pub if_exists: bool,
} }
impl RemoveTokenStatement { impl RemoveAccessStatement {
/// Process this type returning a computed simple Value /// Process this type returning a computed simple Value
pub(crate) async fn compute( pub(crate) async fn compute(
&self, &self,
@ -38,9 +38,9 @@ impl RemoveTokenStatement {
// Clear the cache // Clear the cache
run.clear_cache(); run.clear_cache();
// Get the definition // Get the definition
let tk = run.get_ns_token(opt.ns(), &self.name).await?; let ac = run.get_ns_access(opt.ns(), &self.name).await?;
// Delete the definition // Delete the definition
let key = crate::key::namespace::tk::new(opt.ns(), &tk.name); let key = crate::key::namespace::ac::new(opt.ns(), &ac.name);
run.del(key).await?; run.del(key).await?;
// Ok all good // Ok all good
Ok(Value::None) Ok(Value::None)
@ -51,22 +51,9 @@ impl RemoveTokenStatement {
// Clear the cache // Clear the cache
run.clear_cache(); run.clear_cache();
// Get the definition // Get the definition
let tk = run.get_db_token(opt.ns(), opt.db(), &self.name).await?; let ac = run.get_db_access(opt.ns(), opt.db(), &self.name).await?;
// Delete the definition // Delete the definition
let key = crate::key::database::tk::new(opt.ns(), opt.db(), &tk.name); let key = crate::key::database::ac::new(opt.ns(), opt.db(), &ac.name);
run.del(key).await?;
// Ok all good
Ok(Value::None)
}
Base::Sc(sc) => {
// Claim transaction
let mut run = txn.lock().await;
// Clear the cache
run.clear_cache();
// Get the definition
let tk = run.get_sc_token(opt.ns(), opt.db(), sc, &self.name).await?;
// Delete the definition
let key = crate::key::scope::tk::new(opt.ns(), opt.db(), sc, &tk.name);
run.del(key).await?; run.del(key).await?;
// Ok all good // Ok all good
Ok(Value::None) Ok(Value::None)
@ -77,13 +64,10 @@ impl RemoveTokenStatement {
.await; .await;
match future { match future {
Err(e) if self.if_exists => match e { Err(e) if self.if_exists => match e {
Error::NtNotFound { Error::NaNotFound {
.. ..
} => Ok(Value::None), } => Ok(Value::None),
Error::DtNotFound { Error::DaNotFound {
..
} => Ok(Value::None),
Error::StNotFound {
.. ..
} => Ok(Value::None), } => Ok(Value::None),
e => Err(e), e => Err(e),
@ -93,9 +77,9 @@ impl RemoveTokenStatement {
} }
} }
impl Display for RemoveTokenStatement { impl Display for RemoveAccessStatement {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "REMOVE TOKEN")?; write!(f, "REMOVE ACCESS")?;
if self.if_exists { if self.if_exists {
write!(f, " IF EXISTS")? write!(f, " IF EXISTS")?
} }

View file

@ -1,3 +1,4 @@
mod access;
mod analyzer; mod analyzer;
mod database; mod database;
mod event; mod event;
@ -7,11 +8,10 @@ mod index;
mod model; mod model;
mod namespace; mod namespace;
mod param; mod param;
mod scope;
mod table; mod table;
mod token;
mod user; mod user;
pub use access::RemoveAccessStatement;
pub use analyzer::RemoveAnalyzerStatement; pub use analyzer::RemoveAnalyzerStatement;
pub use database::RemoveDatabaseStatement; pub use database::RemoveDatabaseStatement;
pub use event::RemoveEventStatement; pub use event::RemoveEventStatement;
@ -21,9 +21,7 @@ pub use index::RemoveIndexStatement;
pub use model::RemoveModelStatement; pub use model::RemoveModelStatement;
pub use namespace::RemoveNamespaceStatement; pub use namespace::RemoveNamespaceStatement;
pub use param::RemoveParamStatement; pub use param::RemoveParamStatement;
pub use scope::RemoveScopeStatement;
pub use table::RemoveTableStatement; pub use table::RemoveTableStatement;
pub use token::RemoveTokenStatement;
pub use user::RemoveUserStatement; pub use user::RemoveUserStatement;
use crate::ctx::Context; use crate::ctx::Context;
@ -45,8 +43,7 @@ pub enum RemoveStatement {
Database(RemoveDatabaseStatement), Database(RemoveDatabaseStatement),
Function(RemoveFunctionStatement), Function(RemoveFunctionStatement),
Analyzer(RemoveAnalyzerStatement), Analyzer(RemoveAnalyzerStatement),
Token(RemoveTokenStatement), Access(RemoveAccessStatement),
Scope(RemoveScopeStatement),
Param(RemoveParamStatement), Param(RemoveParamStatement),
Table(RemoveTableStatement), Table(RemoveTableStatement),
Event(RemoveEventStatement), Event(RemoveEventStatement),
@ -73,8 +70,7 @@ impl RemoveStatement {
Self::Namespace(ref v) => v.compute(ctx, opt, txn).await, Self::Namespace(ref v) => v.compute(ctx, opt, txn).await,
Self::Database(ref v) => v.compute(ctx, opt, txn).await, Self::Database(ref v) => v.compute(ctx, opt, txn).await,
Self::Function(ref v) => v.compute(ctx, opt, txn).await, Self::Function(ref v) => v.compute(ctx, opt, txn).await,
Self::Token(ref v) => v.compute(ctx, opt, txn).await, Self::Access(ref v) => v.compute(ctx, opt, txn).await,
Self::Scope(ref v) => v.compute(ctx, opt, txn).await,
Self::Param(ref v) => v.compute(ctx, opt, txn).await, Self::Param(ref v) => v.compute(ctx, opt, txn).await,
Self::Table(ref v) => v.compute(ctx, opt, txn).await, Self::Table(ref v) => v.compute(ctx, opt, txn).await,
Self::Event(ref v) => v.compute(ctx, opt, txn).await, Self::Event(ref v) => v.compute(ctx, opt, txn).await,
@ -93,8 +89,7 @@ impl Display for RemoveStatement {
Self::Namespace(v) => Display::fmt(v, f), Self::Namespace(v) => Display::fmt(v, f),
Self::Database(v) => Display::fmt(v, f), Self::Database(v) => Display::fmt(v, f),
Self::Function(v) => Display::fmt(v, f), Self::Function(v) => Display::fmt(v, f),
Self::Token(v) => Display::fmt(v, f), Self::Access(v) => Display::fmt(v, f),
Self::Scope(v) => Display::fmt(v, f),
Self::Param(v) => Display::fmt(v, f), Self::Param(v) => Display::fmt(v, f),
Self::Table(v) => Display::fmt(v, f), Self::Table(v) => Display::fmt(v, f),
Self::Event(v) => Display::fmt(v, f), Self::Event(v) => Display::fmt(v, f),

View file

@ -1,66 +0,0 @@
use crate::ctx::Context;
use crate::dbs::{Options, Transaction};
use crate::err::Error;
use crate::iam::{Action, ResourceKind};
use crate::sql::{Base, Ident, Value};
use derive::Store;
use revision::revisioned;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter};
#[revisioned(revision = 2)]
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
pub struct RemoveScopeStatement {
pub name: Ident,
#[revision(start = 2)]
pub if_exists: bool,
}
impl RemoveScopeStatement {
/// Process this type returning a computed simple Value
pub(crate) async fn compute(
&self,
_ctx: &Context<'_>,
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
let future = async {
// Allowed to run?
opt.is_allowed(Action::Edit, ResourceKind::Scope, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Clear the cache
run.clear_cache();
// Get the definition
let sc = run.get_sc(opt.ns(), opt.db(), &self.name).await?;
// Delete the definition
let key = crate::key::database::sc::new(opt.ns(), opt.db(), &sc.name);
run.del(key).await?;
// Remove the resource data
let key = crate::key::scope::all::new(opt.ns(), opt.db(), &sc.name);
run.delp(key, u32::MAX).await?;
// Ok all good
Ok(Value::None)
}
.await;
match future {
Err(Error::ScNotFound {
..
}) if self.if_exists => Ok(Value::None),
v => v,
}
}
}
impl Display for RemoveScopeStatement {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "REMOVE SCOPE")?;
if self.if_exists {
write!(f, " IF EXISTS")?
}
write!(f, " {}", self.name)?;
Ok(())
}
}

View file

@ -0,0 +1,2 @@
pub(super) mod opt;
pub(super) mod vec;

View file

@ -0,0 +1,56 @@
use crate::err::Error;
use crate::sql::value::serde::ser;
use crate::sql::Access;
use serde::ser::Impossible;
use serde::ser::Serialize;
#[non_exhaustive]
pub struct Serializer;
impl ser::Serializer for Serializer {
type Ok = Option<Access>;
type Error = Error;
type SerializeSeq = Impossible<Option<Access>, Error>;
type SerializeTuple = Impossible<Option<Access>, Error>;
type SerializeTupleStruct = Impossible<Option<Access>, Error>;
type SerializeTupleVariant = Impossible<Option<Access>, Error>;
type SerializeMap = Impossible<Option<Access>, Error>;
type SerializeStruct = Impossible<Option<Access>, Error>;
type SerializeStructVariant = Impossible<Option<Access>, Error>;
const EXPECTED: &'static str = "an `Option<Access>`";
#[inline]
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
Ok(None)
}
#[inline]
fn serialize_some<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
Ok(Some(Access(value.serialize(ser::string::Serializer.wrap())?)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use ser::Serializer as _;
#[test]
fn none() {
let option: Option<Access> = None;
let serialized = option.serialize(Serializer.wrap()).unwrap();
assert_eq!(option, serialized);
}
#[test]
fn some() {
let option = Some(Access::default());
let serialized = option.serialize(Serializer.wrap()).unwrap();
assert_eq!(option, serialized);
}
}

View file

@ -0,0 +1,79 @@
use crate::err::Error;
use crate::sql::value::serde::ser;
use crate::sql::Access;
use ser::Serializer as _;
use serde::ser::Impossible;
use serde::ser::Serialize;
#[non_exhaustive]
pub struct Serializer;
impl ser::Serializer for Serializer {
type Ok = Vec<Access>;
type Error = Error;
type SerializeSeq = SerializeAccessVec;
type SerializeTuple = Impossible<Vec<Access>, Error>;
type SerializeTupleStruct = Impossible<Vec<Access>, Error>;
type SerializeTupleVariant = Impossible<Vec<Access>, Error>;
type SerializeMap = Impossible<Vec<Access>, Error>;
type SerializeStruct = Impossible<Vec<Access>, Error>;
type SerializeStructVariant = Impossible<Vec<Access>, Error>;
const EXPECTED: &'static str = "a `Access` sequence";
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Error> {
Ok(SerializeAccessVec(Vec::with_capacity(len.unwrap_or_default())))
}
#[inline]
fn serialize_newtype_struct<T>(
self,
_name: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
value.serialize(self.wrap())
}
}
#[non_exhaustive]
pub struct SerializeAccessVec(Vec<Access>);
impl serde::ser::SerializeSeq for SerializeAccessVec {
type Ok = Vec<Access>;
type Error = Error;
fn serialize_element<T>(&mut self, value: &T) -> Result<(), Self::Error>
where
T: Serialize + ?Sized,
{
self.0.push(Access(value.serialize(ser::string::Serializer.wrap())?));
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
Ok(self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty() {
let vec: Vec<Access> = Vec::new();
let serialized = vec.serialize(Serializer.wrap()).unwrap();
assert_eq!(vec, serialized);
}
#[test]
fn vec() {
let vec = vec![Access("foo".to_owned())];
let serialized = vec.serialize(Serializer.wrap()).unwrap();
assert_eq!(vec, serialized);
}
}

View file

@ -0,0 +1,459 @@
use crate::err::Error;
use crate::sql::access_type::{
AccessType, JwtAccess, JwtAccessIssue, JwtAccessVerify, JwtAccessVerifyJwks,
JwtAccessVerifyKey, RecordAccess,
};
use crate::sql::value::serde::ser;
use crate::sql::Algorithm;
use crate::sql::Duration;
use crate::sql::Value;
use ser::Serializer as _;
use serde::ser::Error as _;
use serde::ser::Impossible;
use serde::ser::Serialize;
// Serialize Access Method
#[non_exhaustive]
pub struct Serializer;
impl ser::Serializer for Serializer {
type Ok = AccessType;
type Error = Error;
type SerializeSeq = Impossible<AccessType, Error>;
type SerializeTuple = Impossible<AccessType, Error>;
type SerializeTupleStruct = Impossible<AccessType, Error>;
type SerializeTupleVariant = Impossible<AccessType, Error>;
type SerializeMap = Impossible<AccessType, Error>;
type SerializeStruct = Impossible<AccessType, Error>;
type SerializeStructVariant = Impossible<AccessType, Error>;
const EXPECTED: &'static str = "a `AccessType`";
fn serialize_newtype_variant<T>(
self,
name: &'static str,
_variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
match variant {
"Record" => Ok(AccessType::Record(value.serialize(SerializerRecord.wrap())?)),
"Jwt" => Ok(AccessType::Jwt(value.serialize(SerializerJwt.wrap())?)),
variant => {
Err(Error::custom(format!("unexpected newtype variant `{name}::{variant}`")))
}
}
}
}
// Serialize Record Access
pub struct SerializerRecord;
impl ser::Serializer for SerializerRecord {
type Ok = RecordAccess;
type Error = Error;
type SerializeSeq = Impossible<RecordAccess, Error>;
type SerializeTuple = Impossible<RecordAccess, Error>;
type SerializeTupleStruct = Impossible<RecordAccess, Error>;
type SerializeTupleVariant = Impossible<RecordAccess, Error>;
type SerializeMap = Impossible<RecordAccess, Error>;
type SerializeStruct = SerializeRecord;
type SerializeStructVariant = Impossible<RecordAccess, Error>;
const EXPECTED: &'static str = "a struct `RecordAccess`";
#[inline]
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Error> {
Ok(SerializeRecord::default())
}
}
#[derive(Default)]
#[non_exhaustive]
pub struct SerializeRecord {
pub duration: Option<Duration>,
pub signup: Option<Value>,
pub signin: Option<Value>,
pub jwt: JwtAccess,
}
impl serde::ser::SerializeStruct for SerializeRecord {
type Ok = RecordAccess;
type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
match key {
"duration" => {
self.duration =
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
}
"signup" => {
self.signup = value.serialize(ser::value::opt::Serializer.wrap())?;
}
"signin" => {
self.signin = value.serialize(ser::value::opt::Serializer.wrap())?;
}
"jwt" => {
self.jwt = value.serialize(SerializerJwt.wrap())?;
}
key => {
return Err(Error::custom(format!("unexpected field `RecordAccess::{key}`")));
}
}
Ok(())
}
fn end(self) -> Result<Self::Ok, Error> {
Ok(RecordAccess {
duration: self.duration,
signup: self.signup,
signin: self.signin,
jwt: self.jwt,
})
}
}
// Serialize JWT Access
pub struct SerializerJwt;
impl ser::Serializer for SerializerJwt {
type Ok = JwtAccess;
type Error = Error;
type SerializeSeq = Impossible<JwtAccess, Error>;
type SerializeTuple = Impossible<JwtAccess, Error>;
type SerializeTupleStruct = Impossible<JwtAccess, Error>;
type SerializeTupleVariant = Impossible<JwtAccess, Error>;
type SerializeMap = Impossible<JwtAccess, Error>;
type SerializeStruct = SerializeJwt;
type SerializeStructVariant = Impossible<JwtAccess, Error>;
const EXPECTED: &'static str = "a struct `JwtAccess`";
#[inline]
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Error> {
Ok(SerializeJwt::default())
}
}
#[derive(Default)]
#[non_exhaustive]
pub struct SerializeJwt {
pub verify: JwtAccessVerify,
pub issue: Option<JwtAccessIssue>,
}
impl serde::ser::SerializeStruct for SerializeJwt {
type Ok = JwtAccess;
type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
match key {
"verify" => {
self.verify = value.serialize(SerializerJwtVerify.wrap())?;
}
"issue" => {
self.issue = value.serialize(SerializerJwtIssueOpt.wrap())?;
}
key => {
return Err(Error::custom(format!("unexpected field `JwtAccess::{key}`")));
}
}
Ok(())
}
fn end(self) -> Result<Self::Ok, Error> {
Ok(JwtAccess {
verify: self.verify,
issue: self.issue,
})
}
}
// Serialize JWT Access Verify
pub struct SerializerJwtVerify;
impl ser::Serializer for SerializerJwtVerify {
type Ok = JwtAccessVerify;
type Error = Error;
type SerializeSeq = Impossible<JwtAccessVerify, Error>;
type SerializeTuple = Impossible<JwtAccessVerify, Error>;
type SerializeTupleStruct = Impossible<JwtAccessVerify, Error>;
type SerializeTupleVariant = Impossible<JwtAccessVerify, Error>;
type SerializeMap = Impossible<JwtAccessVerify, Error>;
type SerializeStruct = Impossible<JwtAccessVerify, Error>;
type SerializeStructVariant = Impossible<JwtAccessVerify, Error>;
const EXPECTED: &'static str = "a `JwtAccessVerify`";
fn serialize_newtype_variant<T>(
self,
name: &'static str,
_variant_index: u32,
variant: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
match variant {
"Key" => Ok(JwtAccessVerify::Key(value.serialize(SerializerJwtVerifyKey.wrap())?)),
"Jwks" => Ok(JwtAccessVerify::Jwks(value.serialize(SerializerJwtVerifyJwks.wrap())?)),
variant => {
Err(Error::custom(format!("unexpected newtype variant `{name}::{variant}`")))
}
}
}
}
// Serialize JWT Access Verify Key
pub struct SerializerJwtVerifyKey;
impl ser::Serializer for SerializerJwtVerifyKey {
type Ok = JwtAccessVerifyKey;
type Error = Error;
type SerializeSeq = Impossible<JwtAccessVerifyKey, Error>;
type SerializeTuple = Impossible<JwtAccessVerifyKey, Error>;
type SerializeTupleStruct = Impossible<JwtAccessVerifyKey, Error>;
type SerializeTupleVariant = Impossible<JwtAccessVerifyKey, Error>;
type SerializeMap = Impossible<JwtAccessVerifyKey, Error>;
type SerializeStruct = SerializeJwtVerifyKey;
type SerializeStructVariant = Impossible<JwtAccessVerifyKey, Error>;
const EXPECTED: &'static str = "a struct `JwtAccessVerifyKey`";
#[inline]
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Error> {
Ok(SerializeJwtVerifyKey::default())
}
}
#[derive(Default)]
#[non_exhaustive]
pub struct SerializeJwtVerifyKey {
pub alg: Algorithm,
pub key: String,
}
impl serde::ser::SerializeStruct for SerializeJwtVerifyKey {
type Ok = JwtAccessVerifyKey;
type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
match key {
"alg" => {
self.alg = value.serialize(ser::algorithm::Serializer.wrap())?;
}
"key" => {
self.key = value.serialize(ser::string::Serializer.wrap())?;
}
key => {
return Err(Error::custom(format!("unexpected field `JwtAccessVerifyKey::{key}`")));
}
}
Ok(())
}
fn end(self) -> Result<Self::Ok, Error> {
Ok(JwtAccessVerifyKey {
alg: self.alg,
key: self.key,
})
}
}
// Serialize JWT Access Verify JWKS
pub struct SerializerJwtVerifyJwks;
impl ser::Serializer for SerializerJwtVerifyJwks {
type Ok = JwtAccessVerifyJwks;
type Error = Error;
type SerializeSeq = Impossible<JwtAccessVerifyJwks, Error>;
type SerializeTuple = Impossible<JwtAccessVerifyJwks, Error>;
type SerializeTupleStruct = Impossible<JwtAccessVerifyJwks, Error>;
type SerializeTupleVariant = Impossible<JwtAccessVerifyJwks, Error>;
type SerializeMap = Impossible<JwtAccessVerifyJwks, Error>;
type SerializeStruct = SerializeJwtVerifyJwks;
type SerializeStructVariant = Impossible<JwtAccessVerifyJwks, Error>;
const EXPECTED: &'static str = "a struct `JwtAccessVerifyJwks`";
#[inline]
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Error> {
Ok(SerializeJwtVerifyJwks::default())
}
}
#[derive(Default)]
#[non_exhaustive]
pub struct SerializeJwtVerifyJwks {
pub url: String,
}
impl serde::ser::SerializeStruct for SerializeJwtVerifyJwks {
type Ok = JwtAccessVerifyJwks;
type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
match key {
"url" => {
self.url = value.serialize(ser::string::Serializer.wrap())?;
}
key => {
return Err(Error::custom(format!(
"unexpected field `JwtAccessVerifyJwks::{key}`"
)));
}
}
Ok(())
}
fn end(self) -> Result<Self::Ok, Error> {
Ok(JwtAccessVerifyJwks {
url: self.url,
})
}
}
// Serialize JWT Access Issue
pub struct SerializerJwtIssueOpt;
impl ser::Serializer for SerializerJwtIssueOpt {
type Ok = Option<JwtAccessIssue>;
type Error = Error;
type SerializeSeq = Impossible<Option<JwtAccessIssue>, Error>;
type SerializeTuple = Impossible<Option<JwtAccessIssue>, Error>;
type SerializeTupleStruct = Impossible<Option<JwtAccessIssue>, Error>;
type SerializeTupleVariant = Impossible<Option<JwtAccessIssue>, Error>;
type SerializeMap = Impossible<Option<JwtAccessIssue>, Error>;
type SerializeStruct = Impossible<Option<JwtAccessIssue>, Error>;
type SerializeStructVariant = Impossible<Option<JwtAccessIssue>, Error>;
const EXPECTED: &'static str = "an `Option<JwtAccessIssue>`";
#[inline]
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
Ok(None)
}
#[inline]
fn serialize_some<T>(self, value: &T) -> Result<Self::Ok, Self::Error>
where
T: ?Sized + Serialize,
{
Ok(Some(value.serialize(SerializerJwtIssue.wrap())?))
}
}
pub struct SerializerJwtIssue;
impl ser::Serializer for SerializerJwtIssue {
type Ok = JwtAccessIssue;
type Error = Error;
type SerializeSeq = Impossible<JwtAccessIssue, Error>;
type SerializeTuple = Impossible<JwtAccessIssue, Error>;
type SerializeTupleStruct = Impossible<JwtAccessIssue, Error>;
type SerializeTupleVariant = Impossible<JwtAccessIssue, Error>;
type SerializeMap = Impossible<JwtAccessIssue, Error>;
type SerializeStruct = SerializeJwtIssue;
type SerializeStructVariant = Impossible<JwtAccessIssue, Error>;
const EXPECTED: &'static str = "a struct `JwtAccessIssue`";
#[inline]
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Error> {
Ok(SerializeJwtIssue::default())
}
}
#[derive(Default)]
#[non_exhaustive]
pub struct SerializeJwtIssue {
pub alg: Algorithm,
pub key: String,
pub duration: Option<Duration>,
}
impl serde::ser::SerializeStruct for SerializeJwtIssue {
type Ok = JwtAccessIssue;
type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
match key {
"alg" => {
self.alg = value.serialize(ser::algorithm::Serializer.wrap())?;
}
"key" => {
self.key = value.serialize(ser::string::Serializer.wrap())?;
}
"duration" => {
self.duration =
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
}
key => {
return Err(Error::custom(format!("unexpected field `JwtAccessIssue::{key}`")));
}
}
Ok(())
}
fn end(self) -> Result<Self::Ok, Error> {
Ok(JwtAccessIssue {
duration: self.duration,
alg: self.alg,
key: self.key,
})
}
}

View file

@ -1,3 +1,4 @@
mod access_type;
mod algorithm; mod algorithm;
mod base; mod base;
mod block; mod block;

View file

@ -1,7 +1,7 @@
use crate::err::Error; use crate::err::Error;
use crate::sql::statements::DefineTokenStatement; use crate::sql::access_type::AccessType;
use crate::sql::statements::DefineAccessStatement;
use crate::sql::value::serde::ser; use crate::sql::value::serde::ser;
use crate::sql::Algorithm;
use crate::sql::Base; use crate::sql::Base;
use crate::sql::Ident; use crate::sql::Ident;
use crate::sql::Strand; use crate::sql::Strand;
@ -14,18 +14,18 @@ use serde::ser::Serialize;
pub struct Serializer; pub struct Serializer;
impl ser::Serializer for Serializer { impl ser::Serializer for Serializer {
type Ok = DefineTokenStatement; type Ok = DefineAccessStatement;
type Error = Error; type Error = Error;
type SerializeSeq = Impossible<DefineTokenStatement, Error>; type SerializeSeq = Impossible<DefineAccessStatement, Error>;
type SerializeTuple = Impossible<DefineTokenStatement, Error>; type SerializeTuple = Impossible<DefineAccessStatement, Error>;
type SerializeTupleStruct = Impossible<DefineTokenStatement, Error>; type SerializeTupleStruct = Impossible<DefineAccessStatement, Error>;
type SerializeTupleVariant = Impossible<DefineTokenStatement, Error>; type SerializeTupleVariant = Impossible<DefineAccessStatement, Error>;
type SerializeMap = Impossible<DefineTokenStatement, Error>; type SerializeMap = Impossible<DefineAccessStatement, Error>;
type SerializeStruct = SerializeDefineTokenStatement; type SerializeStruct = SerializeDefineAccessStatement;
type SerializeStructVariant = Impossible<DefineTokenStatement, Error>; type SerializeStructVariant = Impossible<DefineAccessStatement, Error>;
const EXPECTED: &'static str = "a struct `DefineTokenStatement`"; const EXPECTED: &'static str = "a struct `DefineAccessStatement`";
#[inline] #[inline]
fn serialize_struct( fn serialize_struct(
@ -33,23 +33,22 @@ impl ser::Serializer for Serializer {
_name: &'static str, _name: &'static str,
_len: usize, _len: usize,
) -> Result<Self::SerializeStruct, Error> { ) -> Result<Self::SerializeStruct, Error> {
Ok(SerializeDefineTokenStatement::default()) Ok(SerializeDefineAccessStatement::default())
} }
} }
#[derive(Default)] #[derive(Default)]
#[non_exhaustive] #[non_exhaustive]
pub struct SerializeDefineTokenStatement { pub struct SerializeDefineAccessStatement {
name: Ident, name: Ident,
base: Base, base: Base,
kind: Algorithm, kind: AccessType,
code: String,
comment: Option<Strand>, comment: Option<Strand>,
if_not_exists: bool, if_not_exists: bool,
} }
impl serde::ser::SerializeStruct for SerializeDefineTokenStatement { impl serde::ser::SerializeStruct for SerializeDefineAccessStatement {
type Ok = DefineTokenStatement; type Ok = DefineAccessStatement;
type Error = Error; type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error> fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
@ -64,10 +63,7 @@ impl serde::ser::SerializeStruct for SerializeDefineTokenStatement {
self.base = value.serialize(ser::base::Serializer.wrap())?; self.base = value.serialize(ser::base::Serializer.wrap())?;
} }
"kind" => { "kind" => {
self.kind = value.serialize(ser::algorithm::Serializer.wrap())?; self.kind = value.serialize(ser::access_type::Serializer.wrap())?;
}
"code" => {
self.code = value.serialize(ser::string::Serializer.wrap())?;
} }
"comment" => { "comment" => {
self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?; self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?;
@ -77,7 +73,7 @@ impl serde::ser::SerializeStruct for SerializeDefineTokenStatement {
} }
key => { key => {
return Err(Error::custom(format!( return Err(Error::custom(format!(
"unexpected field `DefineTokenStatement::{key}`" "unexpected field `DefineAccessStatement::{key}`"
))); )));
} }
} }
@ -85,11 +81,10 @@ impl serde::ser::SerializeStruct for SerializeDefineTokenStatement {
} }
fn end(self) -> Result<Self::Ok, Error> { fn end(self) -> Result<Self::Ok, Error> {
Ok(DefineTokenStatement { Ok(DefineAccessStatement {
name: self.name, name: self.name,
base: self.base, base: self.base,
kind: self.kind, kind: self.kind,
code: self.code,
comment: self.comment, comment: self.comment,
if_not_exists: self.if_not_exists, if_not_exists: self.if_not_exists,
}) })
@ -102,8 +97,8 @@ mod tests {
#[test] #[test]
fn default() { fn default() {
let stmt = DefineTokenStatement::default(); let stmt = DefineAccessStatement::default();
let value: DefineTokenStatement = stmt.serialize(Serializer.wrap()).unwrap(); let value: DefineAccessStatement = stmt.serialize(Serializer.wrap()).unwrap();
assert_eq!(value, stmt); assert_eq!(value, stmt);
} }
} }

View file

@ -1,3 +1,4 @@
mod access;
mod analyzer; mod analyzer;
mod database; mod database;
mod event; mod event;
@ -6,9 +7,7 @@ mod function;
mod index; mod index;
mod namespace; mod namespace;
mod param; mod param;
mod scope;
mod table; mod table;
mod token;
mod user; mod user;
use crate::err::Error; use crate::err::Error;
@ -59,8 +58,7 @@ impl ser::Serializer for Serializer {
"Analyzer" => { "Analyzer" => {
Ok(DefineStatement::Analyzer(value.serialize(analyzer::Serializer.wrap())?)) Ok(DefineStatement::Analyzer(value.serialize(analyzer::Serializer.wrap())?))
} }
"Token" => Ok(DefineStatement::Token(value.serialize(token::Serializer.wrap())?)), "Access" => Ok(DefineStatement::Access(value.serialize(access::Serializer.wrap())?)),
"Scope" => Ok(DefineStatement::Scope(value.serialize(scope::Serializer.wrap())?)),
"Param" => Ok(DefineStatement::Param(value.serialize(param::Serializer.wrap())?)), "Param" => Ok(DefineStatement::Param(value.serialize(param::Serializer.wrap())?)),
"Table" => Ok(DefineStatement::Table(value.serialize(table::Serializer.wrap())?)), "Table" => Ok(DefineStatement::Table(value.serialize(table::Serializer.wrap())?)),
"Event" => Ok(DefineStatement::Event(value.serialize(event::Serializer.wrap())?)), "Event" => Ok(DefineStatement::Event(value.serialize(event::Serializer.wrap())?)),
@ -108,15 +106,8 @@ mod tests {
} }
#[test] #[test]
fn token() { fn access() {
let stmt = DefineStatement::Token(Default::default()); let stmt = DefineStatement::Access(Default::default());
let serialized = stmt.serialize(Serializer.wrap()).unwrap();
assert_eq!(stmt, serialized);
}
#[test]
fn scope() {
let stmt = DefineStatement::Scope(Default::default());
let serialized = stmt.serialize(Serializer.wrap()).unwrap(); let serialized = stmt.serialize(Serializer.wrap()).unwrap();
assert_eq!(stmt, serialized); assert_eq!(stmt, serialized);
} }

View file

@ -1,115 +0,0 @@
use crate::err::Error;
use crate::sql::statements::DefineScopeStatement;
use crate::sql::value::serde::ser;
use crate::sql::Duration;
use crate::sql::Ident;
use crate::sql::Strand;
use crate::sql::Value;
use ser::Serializer as _;
use serde::ser::Error as _;
use serde::ser::Impossible;
use serde::ser::Serialize;
#[non_exhaustive]
pub struct Serializer;
impl ser::Serializer for Serializer {
type Ok = DefineScopeStatement;
type Error = Error;
type SerializeSeq = Impossible<DefineScopeStatement, Error>;
type SerializeTuple = Impossible<DefineScopeStatement, Error>;
type SerializeTupleStruct = Impossible<DefineScopeStatement, Error>;
type SerializeTupleVariant = Impossible<DefineScopeStatement, Error>;
type SerializeMap = Impossible<DefineScopeStatement, Error>;
type SerializeStruct = SerializeDefineScopeStatement;
type SerializeStructVariant = Impossible<DefineScopeStatement, Error>;
const EXPECTED: &'static str = "a struct `DefineScopeStatement`";
#[inline]
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Error> {
Ok(SerializeDefineScopeStatement::default())
}
}
#[derive(Default)]
#[non_exhaustive]
pub struct SerializeDefineScopeStatement {
name: Ident,
code: String,
session: Option<Duration>,
signup: Option<Value>,
signin: Option<Value>,
comment: Option<Strand>,
if_not_exists: bool,
}
impl serde::ser::SerializeStruct for SerializeDefineScopeStatement {
type Ok = DefineScopeStatement;
type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
match key {
"name" => {
self.name = Ident(value.serialize(ser::string::Serializer.wrap())?);
}
"code" => {
self.code = value.serialize(ser::string::Serializer.wrap())?;
}
"session" => {
self.session =
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
}
"signup" => {
self.signup = value.serialize(ser::value::opt::Serializer.wrap())?;
}
"signin" => {
self.signin = value.serialize(ser::value::opt::Serializer.wrap())?;
}
"comment" => {
self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?;
}
"if_not_exists" => {
self.if_not_exists = value.serialize(ser::primitive::bool::Serializer.wrap())?
}
key => {
return Err(Error::custom(format!(
"unexpected field `DefineScopeStatement::{key}`"
)));
}
}
Ok(())
}
fn end(self) -> Result<Self::Ok, Error> {
Ok(DefineScopeStatement {
name: self.name,
code: self.code,
session: self.session,
signup: self.signup,
signin: self.signin,
comment: self.comment,
if_not_exists: self.if_not_exists,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default() {
let stmt = DefineScopeStatement::default();
let value: DefineScopeStatement = stmt.serialize(Serializer.wrap()).unwrap();
assert_eq!(value, stmt);
}
}

View file

@ -61,7 +61,6 @@ impl ser::Serializer for Serializer {
_len: usize, _len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> { ) -> Result<Self::SerializeTupleVariant, Self::Error> {
match variant { match variant {
"Sc" => Ok(SerializeInfoStatement::with(Which::Sc)),
"Tb" => Ok(SerializeInfoStatement::with(Which::Tb)), "Tb" => Ok(SerializeInfoStatement::with(Which::Tb)),
"User" => Ok(SerializeInfoStatement::with(Which::User)), "User" => Ok(SerializeInfoStatement::with(Which::User)),
variant => Err(Error::custom(format!("unexpected tuple variant `{name}::{variant}`"))), variant => Err(Error::custom(format!("unexpected tuple variant `{name}::{variant}`"))),
@ -71,7 +70,6 @@ impl ser::Serializer for Serializer {
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum Which { enum Which {
Sc,
Tb, Tb,
User, User,
} }
@ -79,9 +77,6 @@ enum Which {
impl Display for Which { impl Display for Which {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self { match self {
Which::Sc => {
write!(f, "Sc")
}
Which::Tb => { Which::Tb => {
write!(f, "Tb") write!(f, "Tb")
} }
@ -121,7 +116,7 @@ impl serde::ser::SerializeTupleVariant for SerializeInfoStatement {
(_, 0) => { (_, 0) => {
self.tuple.0 = Some(Ident(value.serialize(ser::string::Serializer.wrap())?)); self.tuple.0 = Some(Ident(value.serialize(ser::string::Serializer.wrap())?));
} }
(Sc, 1) | (Tb, 1) => { (Tb, 1) => {
self.tuple.2 = value.serialize(ser::primitive::bool::Serializer.wrap())?; self.tuple.2 = value.serialize(ser::primitive::bool::Serializer.wrap())?;
} }
(User, 1) => { (User, 1) => {
@ -144,8 +139,6 @@ impl serde::ser::SerializeTupleVariant for SerializeInfoStatement {
fn end(self) -> Result<Self::Ok, Self::Error> { fn end(self) -> Result<Self::Ok, Self::Error> {
use Which::*; use Which::*;
match (self.which, self.tuple.0) { match (self.which, self.tuple.0) {
(Sc, Some(ident)) => Ok(InfoStatement::Sc(ident, self.tuple.2)),
(Sc, None) => Err(Error::custom("`InfoStatement::Sc` missing required value(s)")),
(Tb, Some(ident)) => Ok(InfoStatement::Tb(ident, self.tuple.2)), (Tb, Some(ident)) => Ok(InfoStatement::Tb(ident, self.tuple.2)),
(Tb, None) => Err(Error::custom("`InfoStatement::Tb` missing required value(s)")), (Tb, None) => Err(Error::custom("`InfoStatement::Tb` missing required value(s)")),
(User, Some(ident)) => Ok(InfoStatement::User(ident, self.tuple.1, self.tuple.2)), (User, Some(ident)) => Ok(InfoStatement::User(ident, self.tuple.1, self.tuple.2)),
@ -179,13 +172,6 @@ mod tests {
assert_eq!(stmt, serialized); assert_eq!(stmt, serialized);
} }
#[test]
fn sc() {
let stmt = InfoStatement::Sc(Default::default(), Default::default());
let serialized = stmt.serialize(Serializer.wrap()).unwrap();
assert_eq!(stmt, serialized);
}
#[test] #[test]
fn tb() { fn tb() {
let stmt = InfoStatement::Tb(Default::default(), Default::default()); let stmt = InfoStatement::Tb(Default::default(), Default::default());

View file

@ -1,5 +1,5 @@
use crate::err::Error; use crate::err::Error;
use crate::sql::statements::RemoveTokenStatement; use crate::sql::statements::RemoveAccessStatement;
use crate::sql::value::serde::ser; use crate::sql::value::serde::ser;
use crate::sql::Base; use crate::sql::Base;
use crate::sql::Ident; use crate::sql::Ident;
@ -12,18 +12,18 @@ use serde::ser::Serialize;
pub struct Serializer; pub struct Serializer;
impl ser::Serializer for Serializer { impl ser::Serializer for Serializer {
type Ok = RemoveTokenStatement; type Ok = RemoveAccessStatement;
type Error = Error; type Error = Error;
type SerializeSeq = Impossible<RemoveTokenStatement, Error>; type SerializeSeq = Impossible<RemoveAccessStatement, Error>;
type SerializeTuple = Impossible<RemoveTokenStatement, Error>; type SerializeTuple = Impossible<RemoveAccessStatement, Error>;
type SerializeTupleStruct = Impossible<RemoveTokenStatement, Error>; type SerializeTupleStruct = Impossible<RemoveAccessStatement, Error>;
type SerializeTupleVariant = Impossible<RemoveTokenStatement, Error>; type SerializeTupleVariant = Impossible<RemoveAccessStatement, Error>;
type SerializeMap = Impossible<RemoveTokenStatement, Error>; type SerializeMap = Impossible<RemoveAccessStatement, Error>;
type SerializeStruct = SerializeRemoveTokenStatement; type SerializeStruct = SerializeRemoveAccessStatement;
type SerializeStructVariant = Impossible<RemoveTokenStatement, Error>; type SerializeStructVariant = Impossible<RemoveAccessStatement, Error>;
const EXPECTED: &'static str = "a struct `RemoveTokenStatement`"; const EXPECTED: &'static str = "a struct `RemoveAccessStatement`";
#[inline] #[inline]
fn serialize_struct( fn serialize_struct(
@ -31,20 +31,20 @@ impl ser::Serializer for Serializer {
_name: &'static str, _name: &'static str,
_len: usize, _len: usize,
) -> Result<Self::SerializeStruct, Error> { ) -> Result<Self::SerializeStruct, Error> {
Ok(SerializeRemoveTokenStatement::default()) Ok(SerializeRemoveAccessStatement::default())
} }
} }
#[derive(Default)] #[derive(Default)]
#[non_exhaustive] #[non_exhaustive]
pub struct SerializeRemoveTokenStatement { pub struct SerializeRemoveAccessStatement {
name: Ident, name: Ident,
base: Base, base: Base,
if_exists: bool, if_exists: bool,
} }
impl serde::ser::SerializeStruct for SerializeRemoveTokenStatement { impl serde::ser::SerializeStruct for SerializeRemoveAccessStatement {
type Ok = RemoveTokenStatement; type Ok = RemoveAccessStatement;
type Error = Error; type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error> fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
@ -63,7 +63,7 @@ impl serde::ser::SerializeStruct for SerializeRemoveTokenStatement {
} }
key => { key => {
return Err(Error::custom(format!( return Err(Error::custom(format!(
"unexpected field `RemoveTokenStatement::{key}`" "unexpected field `RemoveAccessStatement::{key}`"
))); )));
} }
} }
@ -71,7 +71,7 @@ impl serde::ser::SerializeStruct for SerializeRemoveTokenStatement {
} }
fn end(self) -> Result<Self::Ok, Error> { fn end(self) -> Result<Self::Ok, Error> {
Ok(RemoveTokenStatement { Ok(RemoveAccessStatement {
name: self.name, name: self.name,
base: self.base, base: self.base,
if_exists: self.if_exists, if_exists: self.if_exists,
@ -85,8 +85,8 @@ mod tests {
#[test] #[test]
fn default() { fn default() {
let stmt = RemoveTokenStatement::default(); let stmt = RemoveAccessStatement::default();
let value: RemoveTokenStatement = stmt.serialize(Serializer.wrap()).unwrap(); let value: RemoveAccessStatement = stmt.serialize(Serializer.wrap()).unwrap();
assert_eq!(value, stmt); assert_eq!(value, stmt);
} }
} }

View file

@ -1,3 +1,4 @@
mod access;
mod analyzer; mod analyzer;
mod database; mod database;
mod event; mod event;
@ -6,9 +7,7 @@ mod function;
mod index; mod index;
mod namespace; mod namespace;
mod param; mod param;
mod scope;
mod table; mod table;
mod token;
mod user; mod user;
use crate::err::Error; use crate::err::Error;
@ -59,8 +58,7 @@ impl ser::Serializer for Serializer {
"Analyzer" => { "Analyzer" => {
Ok(RemoveStatement::Analyzer(value.serialize(analyzer::Serializer.wrap())?)) Ok(RemoveStatement::Analyzer(value.serialize(analyzer::Serializer.wrap())?))
} }
"Token" => Ok(RemoveStatement::Token(value.serialize(token::Serializer.wrap())?)), "Access" => Ok(RemoveStatement::Access(value.serialize(access::Serializer.wrap())?)),
"Scope" => Ok(RemoveStatement::Scope(value.serialize(scope::Serializer.wrap())?)),
"Param" => Ok(RemoveStatement::Param(value.serialize(param::Serializer.wrap())?)), "Param" => Ok(RemoveStatement::Param(value.serialize(param::Serializer.wrap())?)),
"Table" => Ok(RemoveStatement::Table(value.serialize(table::Serializer.wrap())?)), "Table" => Ok(RemoveStatement::Table(value.serialize(table::Serializer.wrap())?)),
"Event" => Ok(RemoveStatement::Event(value.serialize(event::Serializer.wrap())?)), "Event" => Ok(RemoveStatement::Event(value.serialize(event::Serializer.wrap())?)),
@ -108,15 +106,8 @@ mod tests {
} }
#[test] #[test]
fn token() { fn access() {
let stmt = RemoveStatement::Token(Default::default()); let stmt = RemoveStatement::Access(Default::default());
let serialized = stmt.serialize(Serializer.wrap()).unwrap();
assert_eq!(stmt, serialized);
}
#[test]
fn scope() {
let stmt = RemoveStatement::Scope(Default::default());
let serialized = stmt.serialize(Serializer.wrap()).unwrap(); let serialized = stmt.serialize(Serializer.wrap()).unwrap();
assert_eq!(stmt, serialized); assert_eq!(stmt, serialized);
} }

View file

@ -1,86 +0,0 @@
use crate::err::Error;
use crate::sql::statements::RemoveScopeStatement;
use crate::sql::value::serde::ser;
use crate::sql::Ident;
use ser::Serializer as _;
use serde::ser::Error as _;
use serde::ser::Impossible;
use serde::ser::Serialize;
#[non_exhaustive]
pub struct Serializer;
impl ser::Serializer for Serializer {
type Ok = RemoveScopeStatement;
type Error = Error;
type SerializeSeq = Impossible<RemoveScopeStatement, Error>;
type SerializeTuple = Impossible<RemoveScopeStatement, Error>;
type SerializeTupleStruct = Impossible<RemoveScopeStatement, Error>;
type SerializeTupleVariant = Impossible<RemoveScopeStatement, Error>;
type SerializeMap = Impossible<RemoveScopeStatement, Error>;
type SerializeStruct = SerializeRemoveScopeStatement;
type SerializeStructVariant = Impossible<RemoveScopeStatement, Error>;
const EXPECTED: &'static str = "a struct `RemoveScopeStatement`";
#[inline]
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Error> {
Ok(SerializeRemoveScopeStatement::default())
}
}
#[derive(Default)]
#[non_exhaustive]
pub struct SerializeRemoveScopeStatement {
name: Ident,
if_exists: bool,
}
impl serde::ser::SerializeStruct for SerializeRemoveScopeStatement {
type Ok = RemoveScopeStatement;
type Error = Error;
fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<(), Error>
where
T: ?Sized + Serialize,
{
match key {
"name" => {
self.name = Ident(value.serialize(ser::string::Serializer.wrap())?);
}
"if_exists" => {
self.if_exists = value.serialize(ser::primitive::bool::Serializer.wrap())?;
}
key => {
return Err(Error::custom(format!(
"unexpected field `RemoveScopeStatement::{key}`"
)));
}
}
Ok(())
}
fn end(self) -> Result<Self::Ok, Error> {
Ok(RemoveScopeStatement {
name: self.name,
if_exists: self.if_exists,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default() {
let stmt = RemoveScopeStatement::default();
let value: RemoveScopeStatement = stmt.serialize(Serializer.wrap()).unwrap();
assert_eq!(value, stmt);
}
}

View file

@ -56,7 +56,9 @@ pub fn could_be_reserved(s: &str) -> bool {
/// A map for mapping keyword strings to a tokenkind, /// A map for mapping keyword strings to a tokenkind,
pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map! { pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map! {
// Keywords // Keywords
UniCase::ascii("ACCESS") => TokenKind::Keyword(Keyword::Access),
UniCase::ascii("AFTER") => TokenKind::Keyword(Keyword::After), UniCase::ascii("AFTER") => TokenKind::Keyword(Keyword::After),
UniCase::ascii("ALGORITHM") => TokenKind::Keyword(Keyword::Algorithm),
UniCase::ascii("ALL") => TokenKind::Keyword(Keyword::All), UniCase::ascii("ALL") => TokenKind::Keyword(Keyword::All),
UniCase::ascii("ANALYZE") => TokenKind::Keyword(Keyword::Analyze), UniCase::ascii("ANALYZE") => TokenKind::Keyword(Keyword::Analyze),
UniCase::ascii("ANALYZER") => TokenKind::Keyword(Keyword::Analyzer), UniCase::ascii("ANALYZER") => TokenKind::Keyword(Keyword::Analyzer),
@ -132,6 +134,9 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
UniCase::ascii("INTO") => TokenKind::Keyword(Keyword::Into), UniCase::ascii("INTO") => TokenKind::Keyword(Keyword::Into),
UniCase::ascii("IF") => TokenKind::Keyword(Keyword::If), UniCase::ascii("IF") => TokenKind::Keyword(Keyword::If),
UniCase::ascii("IS") => TokenKind::Keyword(Keyword::Is), UniCase::ascii("IS") => TokenKind::Keyword(Keyword::Is),
UniCase::ascii("ISSUER") => TokenKind::Keyword(Keyword::Issuer),
UniCase::ascii("JWT") => TokenKind::Keyword(Keyword::Jwt),
UniCase::ascii("JWKS") => TokenKind::Keyword(Keyword::Jwks),
UniCase::ascii("KEY") => TokenKind::Keyword(Keyword::Key), UniCase::ascii("KEY") => TokenKind::Keyword(Keyword::Key),
UniCase::ascii("KEEP_PRUNED_CONNECTIONS") => TokenKind::Keyword(Keyword::KeepPrunedConnections), UniCase::ascii("KEEP_PRUNED_CONNECTIONS") => TokenKind::Keyword(Keyword::KeepPrunedConnections),
UniCase::ascii("KILL") => TokenKind::Keyword(Keyword::Kill), UniCase::ascii("KILL") => TokenKind::Keyword(Keyword::Kill),
@ -214,6 +219,7 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
UniCase::ascii("UNSET") => TokenKind::Keyword(Keyword::Unset), UniCase::ascii("UNSET") => TokenKind::Keyword(Keyword::Unset),
UniCase::ascii("UPDATE") => TokenKind::Keyword(Keyword::Update), UniCase::ascii("UPDATE") => TokenKind::Keyword(Keyword::Update),
UniCase::ascii("UPPERCASE") => TokenKind::Keyword(Keyword::Uppercase), UniCase::ascii("UPPERCASE") => TokenKind::Keyword(Keyword::Uppercase),
UniCase::ascii("URL") => TokenKind::Keyword(Keyword::Url),
UniCase::ascii("USE") => TokenKind::Keyword(Keyword::Use), UniCase::ascii("USE") => TokenKind::Keyword(Keyword::Use),
UniCase::ascii("USER") => TokenKind::Keyword(Keyword::User), UniCase::ascii("USER") => TokenKind::Keyword(Keyword::User),
UniCase::ascii("VALUE") => TokenKind::Keyword(Keyword::Value), UniCase::ascii("VALUE") => TokenKind::Keyword(Keyword::Value),
@ -338,7 +344,6 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
UniCase::ascii("RS256") => TokenKind::Algorithm(Algorithm::Rs256), UniCase::ascii("RS256") => TokenKind::Algorithm(Algorithm::Rs256),
UniCase::ascii("RS384") => TokenKind::Algorithm(Algorithm::Rs384), UniCase::ascii("RS384") => TokenKind::Algorithm(Algorithm::Rs384),
UniCase::ascii("RS512") => TokenKind::Algorithm(Algorithm::Rs512), UniCase::ascii("RS512") => TokenKind::Algorithm(Algorithm::Rs512),
UniCase::ascii("JWKS") => jwks_token_kind(), // Necessary because `phf_map!` doesn't support `cfg` attributes
// Distance // Distance
UniCase::ascii("CHEBYSHEV") => TokenKind::Distance(DistanceKind::Chebyshev), UniCase::ascii("CHEBYSHEV") => TokenKind::Distance(DistanceKind::Chebyshev),
@ -360,11 +365,3 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
// Change Feed keywords // Change Feed keywords
UniCase::ascii("ORIGINAL") => TokenKind::ChangeFeedInclude(ChangeFeedInclude::Original), UniCase::ascii("ORIGINAL") => TokenKind::ChangeFeedInclude(ChangeFeedInclude::Original),
}; };
const fn jwks_token_kind() -> TokenKind {
#[cfg(feature = "jwks")]
let token = TokenKind::Algorithm(Algorithm::Jwks);
#[cfg(not(feature = "jwks"))]
let token = TokenKind::Identifier;
token
}

View file

@ -226,8 +226,8 @@ pub(crate) static PATHS: phf::Map<UniCase<&'static str>, PathKind> = phf_map! {
UniCase::ascii("session::ip") => PathKind::Function, UniCase::ascii("session::ip") => PathKind::Function,
UniCase::ascii("session::ns") => PathKind::Function, UniCase::ascii("session::ns") => PathKind::Function,
UniCase::ascii("session::origin") => PathKind::Function, UniCase::ascii("session::origin") => PathKind::Function,
UniCase::ascii("session::sc") => PathKind::Function, UniCase::ascii("session::ac") => PathKind::Function,
UniCase::ascii("session::sd") => PathKind::Function, UniCase::ascii("session::rd") => PathKind::Function,
UniCase::ascii("session::token") => PathKind::Function, UniCase::ascii("session::token") => PathKind::Function,
// //
UniCase::ascii("string::concat") => PathKind::Function, UniCase::ascii("string::concat") => PathKind::Function,

View file

@ -1,26 +1,30 @@
use reblessive::Stk; use reblessive::Stk;
use crate::sql::access_type::JwtAccessVerify;
use crate::sql::index::HnswParams; use crate::sql::index::HnswParams;
use crate::{ use crate::{
sql::{ sql::{
access_type,
base::Base,
filter::Filter, filter::Filter,
index::{Distance, VectorType}, index::{Distance, VectorType},
statements::{ statements::{
DefineAnalyzerStatement, DefineDatabaseStatement, DefineEventStatement, DefineAccessStatement, DefineAnalyzerStatement, DefineDatabaseStatement,
DefineFieldStatement, DefineFunctionStatement, DefineIndexStatement, DefineEventStatement, DefineFieldStatement, DefineFunctionStatement,
DefineNamespaceStatement, DefineParamStatement, DefineScopeStatement, DefineStatement, DefineIndexStatement, DefineNamespaceStatement, DefineParamStatement, DefineStatement,
DefineTableStatement, DefineTokenStatement, DefineUserStatement, DefineTableStatement, DefineUserStatement,
}, },
table_type, table_type,
tokenizer::Tokenizer, tokenizer::Tokenizer,
Ident, Idioms, Index, Kind, Param, Permissions, Scoring, Strand, TableType, Values, AccessType, Ident, Idioms, Index, Kind, Param, Permissions, Scoring, Strand, TableType,
Values,
}, },
syn::{ syn::{
parser::{ parser::{
mac::{expected, unexpected}, mac::{expected, unexpected},
ParseResult, Parser, ParseResult, Parser,
}, },
token::{t, TokenKind}, token::{t, Keyword, TokenKind},
}, },
}; };
@ -31,8 +35,8 @@ impl Parser<'_> {
t!("DATABASE") => self.parse_define_database().map(DefineStatement::Database), t!("DATABASE") => self.parse_define_database().map(DefineStatement::Database),
t!("FUNCTION") => self.parse_define_function(ctx).await.map(DefineStatement::Function), t!("FUNCTION") => self.parse_define_function(ctx).await.map(DefineStatement::Function),
t!("USER") => self.parse_define_user().map(DefineStatement::User), t!("USER") => self.parse_define_user().map(DefineStatement::User),
t!("TOKEN") => self.parse_define_token().map(DefineStatement::Token), t!("TOKEN") => self.parse_define_token().map(DefineStatement::Access),
t!("SCOPE") => self.parse_define_scope(ctx).await.map(DefineStatement::Scope), t!("SCOPE") => self.parse_define_scope(ctx).await.map(DefineStatement::Access),
t!("PARAM") => self.parse_define_param(ctx).await.map(DefineStatement::Param), t!("PARAM") => self.parse_define_param(ctx).await.map(DefineStatement::Param),
t!("TABLE") => self.parse_define_table(ctx).await.map(DefineStatement::Table), t!("TABLE") => self.parse_define_table(ctx).await.map(DefineStatement::Table),
t!("EVENT") => { t!("EVENT") => {
@ -43,6 +47,7 @@ impl Parser<'_> {
} }
t!("INDEX") => self.parse_define_index().map(DefineStatement::Index), t!("INDEX") => self.parse_define_index().map(DefineStatement::Index),
t!("ANALYZER") => self.parse_define_analyzer().map(DefineStatement::Analyzer), t!("ANALYZER") => self.parse_define_analyzer().map(DefineStatement::Analyzer),
t!("ACCESS") => self.parse_define_access(ctx).await.map(DefineStatement::Access),
x => unexpected!(self, x, "a define statement keyword"), x => unexpected!(self, x, "a define statement keyword"),
} }
} }
@ -211,7 +216,10 @@ impl Parser<'_> {
Ok(res) Ok(res)
} }
pub fn parse_define_token(&mut self) -> ParseResult<DefineTokenStatement> { pub async fn parse_define_access(
&mut self,
stk: &mut Stk,
) -> ParseResult<DefineAccessStatement> {
let if_not_exists = if self.eat(t!("IF")) { let if_not_exists = if self.eat(t!("IF")) {
expected!(self, t!("NOT")); expected!(self, t!("NOT"));
expected!(self, t!("EXISTS")); expected!(self, t!("EXISTS"));
@ -221,9 +229,10 @@ impl Parser<'_> {
}; };
let name = self.next_token_value()?; let name = self.next_token_value()?;
expected!(self, t!("ON")); expected!(self, t!("ON"));
let base = self.parse_base(true)?; // TODO: Parse base should no longer take an argument.
let base = self.parse_base(false)?;
let mut res = DefineTokenStatement { let mut res = DefineAccessStatement {
name, name,
base, base,
if_not_exists, if_not_exists,
@ -236,17 +245,49 @@ impl Parser<'_> {
self.pop_peek(); self.pop_peek();
res.comment = Some(self.next_token_value()?); res.comment = Some(self.next_token_value()?);
} }
t!("VALUE") => {
self.pop_peek();
res.code = self.next_token_value::<Strand>()?.0;
}
t!("TYPE") => { t!("TYPE") => {
self.pop_peek(); self.pop_peek();
match self.next().kind { match self.peek_kind() {
TokenKind::Algorithm(x) => { t!("JWT") => {
res.kind = x; self.pop_peek();
res.kind = AccessType::Jwt(self.parse_jwt(None)?);
} }
x => unexpected!(self, x, "a token algorithm"), t!("RECORD") => {
self.pop_peek();
let mut ac = access_type::RecordAccess {
..Default::default()
};
loop {
match self.peek_kind() {
t!("DURATION") => {
self.pop_peek();
ac.duration = Some(self.next_token_value()?);
// By default, token duration matches session duration
// The token duration can be modified in the WITH JWT clause
if let Some(ref mut iss) = ac.jwt.issue {
iss.duration = ac.duration;
}
}
t!("SIGNUP") => {
self.pop_peek();
ac.signup =
Some(stk.run(|stk| self.parse_value(stk)).await?);
}
t!("SIGNIN") => {
self.pop_peek();
ac.signin =
Some(stk.run(|stk| self.parse_value(stk)).await?);
}
_ => break,
}
}
if self.eat(t!("WITH")) {
expected!(self, t!("JWT"));
ac.jwt = self.parse_jwt(Some(AccessType::Record(ac.clone())))?;
}
res.kind = AccessType::Record(ac);
}
_ => break,
} }
} }
_ => break, _ => break,
@ -256,7 +297,8 @@ impl Parser<'_> {
Ok(res) Ok(res)
} }
pub async fn parse_define_scope(&mut self, stk: &mut Stk) -> ParseResult<DefineScopeStatement> { // TODO(gguillemas): Deprecated in 2.0.0. Drop this in 3.0.0 in favor of DEFINE ACCESS
pub fn parse_define_token(&mut self) -> ParseResult<DefineAccessStatement> {
let if_not_exists = if self.eat(t!("IF")) { let if_not_exists = if self.eat(t!("IF")) {
expected!(self, t!("NOT")); expected!(self, t!("NOT"));
expected!(self, t!("EXISTS")); expected!(self, t!("EXISTS"));
@ -265,13 +307,130 @@ impl Parser<'_> {
false false
}; };
let name = self.next_token_value()?; let name = self.next_token_value()?;
let mut res = DefineScopeStatement { expected!(self, t!("ON"));
let base = self.parse_base(true)?;
let mut res = DefineAccessStatement {
name, name,
code: DefineScopeStatement::random_code(), base: base.clone(),
if_not_exists, if_not_exists,
..Default::default() ..Default::default()
}; };
match base {
// DEFINE TOKEN ON SCOPE is now record access with JWT
Base::Sc(_) => {
res.base = Base::Db;
let mut ac = access_type::RecordAccess {
..Default::default()
};
ac.jwt.issue = None;
loop {
match self.peek_kind() {
t!("COMMENT") => {
self.pop_peek();
res.comment = Some(self.next_token_value()?);
}
// For backward compatibility, value is always expected after type
// This matches the display format of the legacy statement
t!("TYPE") => {
self.pop_peek();
match self.next().kind {
TokenKind::Algorithm(alg) => {
expected!(self, t!("VALUE"));
ac.jwt.verify = access_type::JwtAccessVerify::Key(
access_type::JwtAccessVerifyKey {
alg,
key: self.next_token_value::<Strand>()?.0,
},
);
}
TokenKind::Keyword(Keyword::Jwks) => {
expected!(self, t!("VALUE"));
ac.jwt.verify = access_type::JwtAccessVerify::Jwks(
access_type::JwtAccessVerifyJwks {
url: self.next_token_value::<Strand>()?.0,
},
);
}
x => unexpected!(self, x, "a token algorithm or 'JWKS'"),
}
}
_ => break,
}
}
res.kind = AccessType::Record(ac);
}
// DEFINE TOKEN anywhere else is now JWT access
_ => {
let mut ac = access_type::JwtAccess {
issue: None,
..Default::default()
};
loop {
match self.peek_kind() {
t!("COMMENT") => {
self.pop_peek();
res.comment = Some(self.next_token_value()?);
}
// For backward compatibility, value is always expected after type
// This matches the display format of the legacy statement
t!("TYPE") => {
self.pop_peek();
match self.next().kind {
TokenKind::Algorithm(alg) => {
expected!(self, t!("VALUE"));
ac.verify = access_type::JwtAccessVerify::Key(
access_type::JwtAccessVerifyKey {
alg,
key: self.next_token_value::<Strand>()?.0,
},
);
}
TokenKind::Keyword(Keyword::Jwks) => {
expected!(self, t!("VALUE"));
ac.verify = access_type::JwtAccessVerify::Jwks(
access_type::JwtAccessVerifyJwks {
url: self.next_token_value::<Strand>()?.0,
},
);
}
x => unexpected!(self, x, "a token algorithm or 'JWKS'"),
}
}
_ => break,
}
}
res.kind = AccessType::Jwt(ac);
}
}
Ok(res)
}
// TODO(gguillemas): Deprecated in 2.0.0. Drop this in 3.0.0 in favor of DEFINE ACCESS
pub async fn parse_define_scope(
&mut self,
stk: &mut Stk,
) -> ParseResult<DefineAccessStatement> {
let if_not_exists = if self.eat(t!("IF")) {
expected!(self, t!("NOT"));
expected!(self, t!("EXISTS"));
true
} else {
false
};
let name = self.next_token_value()?;
let mut res = DefineAccessStatement {
name,
base: Base::Db,
if_not_exists,
..Default::default()
};
let mut ac = access_type::RecordAccess {
..Default::default()
};
loop { loop {
match self.peek_kind() { match self.peek_kind() {
t!("COMMENT") => { t!("COMMENT") => {
@ -280,20 +439,26 @@ impl Parser<'_> {
} }
t!("SESSION") => { t!("SESSION") => {
self.pop_peek(); self.pop_peek();
res.session = Some(self.next_token_value()?); ac.duration = Some(self.next_token_value()?);
// By default, token duration matches session duration.
if let Some(ref mut iss) = ac.jwt.issue {
iss.duration = ac.duration;
}
} }
t!("SIGNUP") => { t!("SIGNUP") => {
self.pop_peek(); self.pop_peek();
res.signup = Some(stk.run(|stk| self.parse_value(stk)).await?); ac.signup = Some(stk.run(|stk| self.parse_value(stk)).await?);
} }
t!("SIGNIN") => { t!("SIGNIN") => {
self.pop_peek(); self.pop_peek();
res.signin = Some(stk.run(|stk| self.parse_value(stk)).await?); ac.signin = Some(stk.run(|stk| self.parse_value(stk)).await?);
} }
_ => break, _ => break,
} }
} }
res.kind = AccessType::Record(ac);
Ok(res) Ok(res)
} }
@ -914,4 +1079,110 @@ impl Parser<'_> {
} }
Ok(Kind::Record(names)) Ok(Kind::Record(names))
} }
pub fn parse_jwt(&mut self, ac: Option<AccessType>) -> ParseResult<access_type::JwtAccess> {
let mut res = access_type::JwtAccess {
// By default, a JWT access method is only used to verify.
issue: None,
..Default::default()
};
let mut iss = access_type::JwtAccessIssue {
..Default::default()
};
// If an access method was passed, inherit any relevant defaults.
// This will become a match statement whenever more access methods are available.
if let Some(AccessType::Record(ac)) = ac {
// By default, token duration is inherited from session duration in record access.
iss.duration = ac.duration;
}
match self.peek_kind() {
t!("ALGORITHM") => {
self.pop_peek();
match self.next().kind {
TokenKind::Algorithm(alg) => match self.next().kind {
t!("KEY") => {
let key = self.next_token_value::<Strand>()?.0;
res.verify = access_type::JwtAccessVerify::Key(
access_type::JwtAccessVerifyKey {
alg,
key: key.to_owned(),
},
);
// Currently, issuer and verifier must use the same algorithm.
iss.alg = alg;
// If the algorithm is symmetric, the issuer and verifier keys are the same.
// For asymmetric algorithms, the key needs to be explicitly defined.
if alg.is_symmetric() {
iss.key = key;
// Since all the issuer data is known, it can already be assigned.
// Cloning allows updating the original with any explicit issuer data.
res.issue = Some(iss.clone());
}
}
x => unexpected!(self, x, "a key"),
},
x => unexpected!(self, x, "a valid algorithm"),
}
}
t!("URL") => {
self.pop_peek();
let url = self.next_token_value::<Strand>()?.0;
res.verify = access_type::JwtAccessVerify::Jwks(access_type::JwtAccessVerifyJwks {
url,
});
}
x => unexpected!(self, x, "`ALGORITHM`, or `URL`"),
}
if self.eat(t!("WITH")) {
expected!(self, t!("ISSUER"));
loop {
match self.peek_kind() {
t!("ALGORITHM") => {
self.pop_peek();
match self.next().kind {
TokenKind::Algorithm(alg) => {
// If an algorithm is already defined, a different value is not expected.
if let JwtAccessVerify::Key(ref ver) = res.verify {
if alg != ver.alg {
unexpected!(
self,
t!("ALGORITHM"),
"a compatible algorithm or no algorithm"
);
}
}
iss.alg = alg;
}
x => unexpected!(self, x, "a valid algorithm"),
}
}
t!("KEY") => {
self.pop_peek();
let key = self.next_token_value::<Strand>()?.0;
// If the algorithm is symmetric and a key is already defined, a different key is not expected.
if let JwtAccessVerify::Key(ref ver) = res.verify {
if ver.alg.is_symmetric() && key != ver.key {
unexpected!(self, t!("KEY"), "a symmetric key or no key");
}
}
iss.key = key;
}
t!("DURATION") => {
self.pop_peek();
iss.duration = Some(self.next_token_value()?);
}
_ => break,
}
}
res.issue = Some(iss);
}
Ok(res)
}
} }

View file

@ -438,10 +438,6 @@ impl Parser<'_> {
t!("ROOT") => InfoStatement::Root(false), t!("ROOT") => InfoStatement::Root(false),
t!("NAMESPACE") => InfoStatement::Ns(false), t!("NAMESPACE") => InfoStatement::Ns(false),
t!("DATABASE") => InfoStatement::Db(false), t!("DATABASE") => InfoStatement::Db(false),
t!("SCOPE") => {
let ident = self.next_token_value()?;
InfoStatement::Sc(ident, false)
}
t!("TABLE") => { t!("TABLE") => {
let ident = self.next_token_value()?; let ident = self.next_token_value()?;
InfoStatement::Tb(ident, false) InfoStatement::Tb(ident, false)

View file

@ -307,9 +307,10 @@ impl Parser<'_> {
} }
} }
// TODO(gguillemas): Deprecated in 2.0.0. Kept for backward compatibility. Drop it in 3.0.0.
/// Parses a base /// Parses a base
/// ///
/// So either `NAMESPACE`, ~DATABASE`, `ROOT`, or `SCOPE` if `scope_allowed` is true. /// So either `NAMESPACE`, `DATABASE`, `ROOT`, or `SCOPE` if `scope_allowed` is true.
/// ///
/// # Parser state /// # Parser state
/// Expects the next keyword to be a base. /// Expects the next keyword to be a base.
@ -327,9 +328,9 @@ impl Parser<'_> {
} }
x => { x => {
if scope_allowed { if scope_allowed {
unexpected!(self, x, "'NAMEPSPACE', 'DATABASE', 'ROOT', 'SCOPE' or 'KV'") unexpected!(self, x, "'NAMEPSPACE', 'DATABASE', 'ROOT' or 'SCOPE'")
} else { } else {
unexpected!(self, x, "'NAMEPSPACE', 'DATABASE', 'ROOT', or 'KV'") unexpected!(self, x, "'NAMEPSPACE', 'DATABASE' or 'ROOT'")
} }
} }
} }

View file

@ -1,9 +1,9 @@
use crate::{ use crate::{
sql::{ sql::{
statements::{ statements::{
remove::RemoveAnalyzerStatement, RemoveDatabaseStatement, RemoveEventStatement, remove::RemoveAnalyzerStatement, RemoveAccessStatement, RemoveDatabaseStatement,
RemoveFieldStatement, RemoveFunctionStatement, RemoveIndexStatement, RemoveEventStatement, RemoveFieldStatement, RemoveFunctionStatement,
RemoveNamespaceStatement, RemoveParamStatement, RemoveScopeStatement, RemoveStatement, RemoveIndexStatement, RemoveNamespaceStatement, RemoveParamStatement, RemoveStatement,
RemoveUserStatement, RemoveUserStatement,
}, },
Param, Param,
@ -66,7 +66,7 @@ impl Parser<'_> {
if_exists, if_exists,
}) })
} }
t!("TOKEN") => { t!("ACCESS") => {
let if_exists = if self.eat(t!("IF")) { let if_exists = if self.eat(t!("IF")) {
expected!(self, t!("EXISTS")); expected!(self, t!("EXISTS"));
true true
@ -75,28 +75,14 @@ impl Parser<'_> {
}; };
let name = self.next_token_value()?; let name = self.next_token_value()?;
expected!(self, t!("ON")); expected!(self, t!("ON"));
let base = self.parse_base(true)?; let base = self.parse_base(false)?;
RemoveStatement::Token(crate::sql::statements::RemoveTokenStatement { RemoveStatement::Access(RemoveAccessStatement {
name, name,
base, base,
if_exists, if_exists,
}) })
} }
t!("SCOPE") => {
let if_exists = if self.eat(t!("IF")) {
expected!(self, t!("EXISTS"));
true
} else {
false
};
let name = self.next_token_value()?;
RemoveStatement::Scope(RemoveScopeStatement {
name,
if_exists,
})
}
t!("PARAM") => { t!("PARAM") => {
let if_exists = if self.eat(t!("IF")) { let if_exists = if self.eat(t!("IF")) {
expected!(self, t!("EXISTS")); expected!(self, t!("EXISTS"));

View file

@ -1,5 +1,9 @@
use crate::{ use crate::{
sql::{ sql::{
access_type::{
AccessType, JwtAccess, JwtAccessIssue, JwtAccessVerify, JwtAccessVerifyJwks,
JwtAccessVerifyKey, RecordAccess,
},
block::Entry, block::Entry,
changefeed::ChangeFeed, changefeed::ChangeFeed,
filter::Filter, filter::Filter,
@ -8,15 +12,15 @@ use crate::{
statements::{ statements::{
analyze::AnalyzeStatement, show::ShowSince, show::ShowStatement, sleep::SleepStatement, analyze::AnalyzeStatement, show::ShowSince, show::ShowStatement, sleep::SleepStatement,
BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement, BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement,
CreateStatement, DefineAnalyzerStatement, DefineDatabaseStatement, CreateStatement, DefineAccessStatement, DefineAnalyzerStatement,
DefineEventStatement, DefineFieldStatement, DefineFunctionStatement, DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement,
DefineIndexStatement, DefineNamespaceStatement, DefineParamStatement, DefineStatement, DefineFunctionStatement, DefineIndexStatement, DefineNamespaceStatement,
DefineTableStatement, DefineTokenStatement, DeleteStatement, ForeachStatement, DefineParamStatement, DefineStatement, DefineTableStatement, DeleteStatement,
IfelseStatement, InfoStatement, InsertStatement, KillStatement, OptionStatement, ForeachStatement, IfelseStatement, InfoStatement, InsertStatement, KillStatement,
OutputStatement, RelateStatement, RemoveAnalyzerStatement, RemoveDatabaseStatement, OptionStatement, OutputStatement, RelateStatement, RemoveAccessStatement,
RemoveEventStatement, RemoveFieldStatement, RemoveFunctionStatement, RemoveAnalyzerStatement, RemoveDatabaseStatement, RemoveEventStatement,
RemoveIndexStatement, RemoveNamespaceStatement, RemoveParamStatement, RemoveFieldStatement, RemoveFunctionStatement, RemoveIndexStatement,
RemoveScopeStatement, RemoveStatement, RemoveTableStatement, RemoveTokenStatement, RemoveNamespaceStatement, RemoveParamStatement, RemoveStatement, RemoveTableStatement,
RemoveUserStatement, SelectStatement, SetStatement, ThrowStatement, UpdateStatement, RemoveUserStatement, SelectStatement, SetStatement, ThrowStatement, UpdateStatement,
UseStatement, UseStatement,
}, },
@ -219,26 +223,132 @@ fn parse_define_user() {
assert_eq!(stmt.comment, Some(Strand("*******".to_string()))) assert_eq!(stmt.comment, Some(Strand("*******".to_string())))
} }
// TODO(gguillemas): This test is kept in 2.0.0 for backward compatibility. Drop in 3.0.0.
#[test] #[test]
fn parse_define_token() { fn parse_define_token() {
let res = test_parse!(
parse_stmt,
r#"DEFINE TOKEN a ON DATABASE TYPE EDDSA VALUE "foo" COMMENT "bar""#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::EdDSA,
key: "foo".to_string(),
}),
issue: None,
}),
comment: Some(Strand("bar".to_string())),
if_not_exists: false,
})),
)
}
// TODO(gguillemas): This test is kept in 2.0.0 for backward compatibility. Drop in 3.0.0.
#[test]
fn parse_define_token_on_scope() {
let res = test_parse!( let res = test_parse!(
parse_stmt, parse_stmt,
r#"DEFINE TOKEN a ON SCOPE b TYPE EDDSA VALUE "foo" COMMENT "bar""# r#"DEFINE TOKEN a ON SCOPE b TYPE EDDSA VALUE "foo" COMMENT "bar""#
) )
.unwrap(); .unwrap();
// Manually compare since DefineAccessStatement for record access
// without explicit JWT will create a random signing key during parsing.
let Statement::Define(DefineStatement::Access(stmt)) = res else {
panic!()
};
assert_eq!(stmt.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db); // Scope base is ignored.
assert_eq!(stmt.comment, Some(Strand("bar".to_string())));
assert_eq!(stmt.if_not_exists, false);
match stmt.kind {
AccessType::Record(ac) => {
// A session duration of one hour is set by default.
assert_eq!(ac.duration, Some(Duration::from_hours(1)));
assert_eq!(ac.signup, None);
assert_eq!(ac.signin, None);
match ac.jwt.verify {
JwtAccessVerify::Key(key) => {
assert_eq!(key.alg, Algorithm::EdDSA);
}
_ => panic!(),
}
assert_eq!(ac.jwt.issue, None);
}
_ => panic!(),
}
}
// TODO(gguillemas): This test is kept in 2.0.0 for backward compatibility. Drop in 3.0.0.
#[test]
fn parse_define_token_jwks() {
let res = test_parse!(
parse_stmt,
r#"DEFINE TOKEN a ON DATABASE TYPE JWKS VALUE "http://example.com/.well-known/jwks.json" COMMENT "bar""#
)
.unwrap();
assert_eq!( assert_eq!(
res, res,
Statement::Define(DefineStatement::Token(DefineTokenStatement { Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()), name: Ident("a".to_string()),
base: Base::Sc(Ident("b".to_string())), base: Base::Db,
kind: Algorithm::EdDSA, kind: AccessType::Jwt(JwtAccess {
code: "foo".to_string(), verify: JwtAccessVerify::Jwks(JwtAccessVerifyJwks {
url: "http://example.com/.well-known/jwks.json".to_string(),
}),
issue: None,
}),
comment: Some(Strand("bar".to_string())), comment: Some(Strand("bar".to_string())),
if_not_exists: false, if_not_exists: false,
})) })),
) )
} }
// TODO(gguillemas): This test is kept in 2.0.0 for backward compatibility. Drop in 3.0.0.
#[test]
fn parse_define_token_jwks_on_scope() {
let res = test_parse!(
parse_stmt,
r#"DEFINE TOKEN a ON SCOPE b TYPE JWKS VALUE "http://example.com/.well-known/jwks.json" COMMENT "bar""#
)
.unwrap();
// Manually compare since DefineAccessStatement for record access
// without explicit JWT will create a random signing key during parsing.
let Statement::Define(DefineStatement::Access(stmt)) = res else {
panic!()
};
assert_eq!(stmt.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db); // Scope base is ignored.
assert_eq!(stmt.comment, Some(Strand("bar".to_string())));
assert_eq!(stmt.if_not_exists, false);
match stmt.kind {
AccessType::Record(ac) => {
// A session duration of one hour is set by default.
assert_eq!(ac.duration, Some(Duration::from_hours(1)));
assert_eq!(ac.signup, None);
assert_eq!(ac.signin, None);
match ac.jwt.verify {
JwtAccessVerify::Jwks(jwks) => {
assert_eq!(jwks.url, "http://example.com/.well-known/jwks.json");
}
_ => panic!(),
}
assert_eq!(ac.jwt.issue, None);
}
_ => panic!(),
}
}
// TODO(gguillemas): This test is kept in 2.0.0 for backward compatibility. Drop in 3.0.0.
#[test] #[test]
fn parse_define_scope() { fn parse_define_scope() {
let res = test_parse!( let res = test_parse!(
@ -247,16 +357,568 @@ fn parse_define_scope() {
) )
.unwrap(); .unwrap();
// manually compare since DefineScopeStatement creates a random code in its parser. // Manually compare since DefineAccessStatement for record access
let Statement::Define(DefineStatement::Scope(stmt)) = res else { // without explicit JWT will create a random signing key during parsing.
let Statement::Define(DefineStatement::Access(stmt)) = res else {
panic!() panic!()
}; };
assert_eq!(stmt.name, Ident("a".to_string())); assert_eq!(stmt.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db);
assert_eq!(stmt.comment, Some(Strand("bar".to_string()))); assert_eq!(stmt.comment, Some(Strand("bar".to_string())));
assert_eq!(stmt.session, Some(Duration(std::time::Duration::from_secs(1)))); assert_eq!(stmt.if_not_exists, false);
assert_eq!(stmt.signup, Some(Value::Bool(true))); match stmt.kind {
assert_eq!(stmt.signin, Some(Value::Bool(false))); AccessType::Record(ac) => {
assert_eq!(ac.duration, Some(Duration(std::time::Duration::from_secs(1))));
assert_eq!(ac.signup, Some(Value::Bool(true)));
assert_eq!(ac.signin, Some(Value::Bool(false)));
match ac.jwt.verify {
JwtAccessVerify::Key(key) => {
assert_eq!(key.alg, Algorithm::Hs512);
}
_ => panic!(),
}
match ac.jwt.issue {
Some(iss) => {
assert_eq!(iss.alg, Algorithm::Hs512);
// Token duration matches session duration by default.
assert_eq!(iss.duration, Some(Duration::from_secs(1)));
}
_ => panic!(),
}
}
_ => panic!(),
}
}
#[test]
fn parse_define_access_jwt_key() {
// With comment. Asymmetric verify only.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM EDDSA KEY "foo" COMMENT "bar""#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::EdDSA,
key: "foo".to_string(),
}),
issue: None,
}),
comment: Some(Strand("bar".to_string())),
if_not_exists: false,
})),
)
}
// Asymmetric verify and issue.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM EDDSA KEY "foo" WITH ISSUER KEY "bar""#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::EdDSA,
key: "foo".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::EdDSA,
key: "bar".to_string(),
// Default duration.
duration: Some(Duration::from_hours(1)),
}),
}),
comment: None,
if_not_exists: false,
})),
)
}
// Symmetric verify and implicit issue.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo""#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::Hs256,
key: "foo".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::Hs256,
key: "foo".to_string(),
// Default duration.
duration: Some(Duration::from_hours(1)),
}),
}),
comment: None,
if_not_exists: false,
})),
)
}
// Symmetric verify and explicit issue.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER DURATION 10s"#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::Hs256,
key: "foo".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::Hs256,
key: "foo".to_string(),
duration: Some(Duration::from_secs(10)),
}),
}),
comment: None,
if_not_exists: false,
})),
)
}
// Symmetric verify and explicit issue matching data.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER ALGORITHM HS256 KEY "foo" DURATION 10s"#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::Hs256,
key: "foo".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::Hs256,
key: "foo".to_string(),
duration: Some(Duration::from_secs(10)),
}),
}),
comment: None,
if_not_exists: false,
})),
)
}
// Symmetric verify and explicit issue non-matching data.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER ALGORITHM HS384 KEY "bar" DURATION 10s"#
);
assert!(
res.is_err(),
"Unexpected successful parsing of non-matching verifier and issuer: {:?}",
res
);
}
// Symmetric verify and explicit issue non-matching key.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER KEY "bar" DURATION 10s"#
);
assert!(
res.is_err(),
"Unexpected successful parsing of non-matching verifier and issuer: {:?}",
res
);
}
// Symmetric verify and explicit issue non-matching algorithm.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM HS256 KEY "foo" WITH ISSUER ALGORITHM HS384 DURATION 10s"#
);
assert!(
res.is_err(),
"Unexpected successful parsing of non-matching verifier and issuer: {:?}",
res
);
}
}
#[test]
fn parse_define_access_jwt_jwks() {
// With comment. Verify only.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT URL "http://example.com/.well-known/jwks.json" COMMENT "bar""#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Jwks(JwtAccessVerifyJwks {
url: "http://example.com/.well-known/jwks.json".to_string(),
}),
issue: None,
}),
comment: Some(Strand("bar".to_string())),
if_not_exists: false,
})),
)
}
// Verify and symmetric issuer.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT URL "http://example.com/.well-known/jwks.json" WITH ISSUER ALGORITHM HS384 KEY "foo""#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Jwks(JwtAccessVerifyJwks {
url: "http://example.com/.well-known/jwks.json".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::Hs384,
key: "foo".to_string(),
// Default duration.
duration: Some(Duration::from_hours(1)),
}),
}),
comment: None,
if_not_exists: false,
})),
)
}
// Verify and symmetric issuer with custom duration.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT URL "http://example.com/.well-known/jwks.json" WITH ISSUER ALGORITHM HS384 KEY "foo" DURATION 10s"#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Jwks(JwtAccessVerifyJwks {
url: "http://example.com/.well-known/jwks.json".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::Hs384,
key: "foo".to_string(),
duration: Some(Duration::from_secs(10)),
}),
}),
comment: None,
if_not_exists: false,
})),
)
}
// Verify and asymmetric issuer.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT URL "http://example.com/.well-known/jwks.json" WITH ISSUER ALGORITHM PS256 KEY "foo""#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Jwks(JwtAccessVerifyJwks {
url: "http://example.com/.well-known/jwks.json".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::Ps256,
key: "foo".to_string(),
// Default duration.
duration: Some(Duration::from_hours(1)),
}),
}),
comment: None,
if_not_exists: false,
})),
)
}
// Verify and asymmetric issuer with custom duration.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE JWT URL "http://example.com/.well-known/jwks.json" WITH ISSUER ALGORITHM PS256 KEY "foo" DURATION 10s"#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Jwt(JwtAccess {
verify: JwtAccessVerify::Jwks(JwtAccessVerifyJwks {
url: "http://example.com/.well-known/jwks.json".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::Ps256,
key: "foo".to_string(),
duration: Some(Duration::from_secs(10)),
}),
}),
comment: None,
if_not_exists: false,
})),
)
}
}
#[test]
fn parse_define_access_record() {
// With comment. Nothing is explicitly defined.
{
let res =
test_parse!(parse_stmt, r#"DEFINE ACCESS a ON DB TYPE RECORD COMMENT "bar""#).unwrap();
// Manually compare since DefineAccessStatement for record access
// without explicit JWT will create a random signing key during parsing.
let Statement::Define(DefineStatement::Access(stmt)) = res else {
panic!()
};
assert_eq!(stmt.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db);
assert_eq!(stmt.comment, Some(Strand("bar".to_string())));
assert_eq!(stmt.if_not_exists, false);
match stmt.kind {
AccessType::Record(ac) => {
// A session duration of one hour is set by default.
assert_eq!(ac.duration, Some(Duration::from_hours(1)));
assert_eq!(ac.signup, None);
assert_eq!(ac.signin, None);
match ac.jwt.verify {
JwtAccessVerify::Key(key) => {
assert_eq!(key.alg, Algorithm::Hs512);
}
_ => panic!(),
}
match ac.jwt.issue {
Some(iss) => {
assert_eq!(iss.alg, Algorithm::Hs512);
// A token duration of one hour is set by default.
assert_eq!(iss.duration, Some(Duration::from_hours(1)));
}
_ => panic!(),
}
}
_ => panic!(),
}
}
// Duration and signing queries are explicitly defined.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DB TYPE RECORD DURATION 10s SIGNUP true SIGNIN false"#
)
.unwrap();
// Manually compare since DefineAccessStatement for record access
// without explicit JWT will create a random signing key during parsing.
let Statement::Define(DefineStatement::Access(stmt)) = res else {
panic!()
};
assert_eq!(stmt.name, Ident("a".to_string()));
assert_eq!(stmt.base, Base::Db);
assert_eq!(stmt.comment, None);
assert_eq!(stmt.if_not_exists, false);
match stmt.kind {
AccessType::Record(ac) => {
assert_eq!(ac.duration, Some(Duration(std::time::Duration::from_secs(10))));
assert_eq!(ac.signup, Some(Value::Bool(true)));
assert_eq!(ac.signin, Some(Value::Bool(false)));
match ac.jwt.verify {
JwtAccessVerify::Key(key) => {
assert_eq!(key.alg, Algorithm::Hs512);
}
_ => panic!(),
}
match ac.jwt.issue {
Some(iss) => {
assert_eq!(iss.alg, Algorithm::Hs512);
// Token duration matches session duration by default.
assert_eq!(iss.duration, Some(Duration::from_secs(10)));
}
_ => panic!(),
}
}
_ => panic!(),
}
}
// Verification with JWT is explicitly defined only with symmetric key.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DB TYPE RECORD DURATION 10s WITH JWT ALGORITHM HS384 KEY "foo""#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Record(RecordAccess {
duration: Some(Duration::from_secs(10)),
signup: None,
signin: None,
jwt: JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::Hs384,
key: "foo".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::Hs384,
// Issuer key matches verification key by default in symmetric algorithms.
key: "foo".to_string(),
// Token duration matches session duration by default.
duration: Some(Duration::from_secs(10)),
}),
}
}),
comment: None,
if_not_exists: false,
})),
);
}
// Verification and issuing with JWT are explicitly defined with two different keys.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DB TYPE RECORD DURATION 10s WITH JWT ALGORITHM PS512 KEY "foo" WITH ISSUER KEY "bar""#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Record(RecordAccess {
duration: Some(Duration::from_secs(10)),
signup: None,
signin: None,
jwt: JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::Ps512,
key: "foo".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::Ps512,
key: "bar".to_string(),
// Token duration matches session duration by default.
duration: Some(Duration::from_secs(10)),
}),
}
}),
comment: None,
if_not_exists: false,
})),
);
}
// Verification and issuing with JWT are explicitly defined with two different keys. Token duration is explicitly defined.
{
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DB TYPE RECORD DURATION 10s WITH JWT ALGORITHM RS256 KEY 'foo' WITH ISSUER KEY 'bar' DURATION 1m"#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Record(RecordAccess {
duration: Some(Duration::from_secs(10)),
signup: None,
signin: None,
jwt: JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::Rs256,
key: "foo".to_string(),
}),
issue: Some(JwtAccessIssue {
alg: Algorithm::Rs256,
key: "bar".to_string(),
duration: Some(Duration::from_mins(1)),
}),
}
}),
comment: None,
if_not_exists: false,
})),
);
}
}
fn parse_define_access_record_with_jwt() {
let res = test_parse!(
parse_stmt,
r#"DEFINE ACCESS a ON DATABASE TYPE RECORD WITH JWT ALGORITHM EDDSA KEY "foo" COMMENT "bar""#
)
.unwrap();
assert_eq!(
res,
Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()),
base: Base::Db,
kind: AccessType::Record(RecordAccess {
duration: Some(Duration::from_hours(1)),
signup: None,
signin: None,
jwt: JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::EdDSA,
key: "foo".to_string(),
}),
issue: None,
}
}),
comment: Some(Strand("bar".to_string())),
if_not_exists: false,
})),
)
} }
#[test] #[test]
@ -683,9 +1345,6 @@ fn parse_info() {
let res = test_parse!(parse_stmt, "INFO FOR NS").unwrap(); let res = test_parse!(parse_stmt, "INFO FOR NS").unwrap();
assert_eq!(res, Statement::Info(InfoStatement::Ns(false))); assert_eq!(res, Statement::Info(InfoStatement::Ns(false)));
let res = test_parse!(parse_stmt, "INFO FOR SCOPE scope").unwrap();
assert_eq!(res, Statement::Info(InfoStatement::Sc(Ident("scope".to_owned()), false)));
let res = test_parse!(parse_stmt, "INFO FOR TABLE table").unwrap(); let res = test_parse!(parse_stmt, "INFO FOR TABLE table").unwrap();
assert_eq!(res, Statement::Info(InfoStatement::Tb(Ident("table".to_owned()), false))); assert_eq!(res, Statement::Info(InfoStatement::Tb(Ident("table".to_owned()), false)));
@ -1129,21 +1788,12 @@ fn parse_remove() {
})) }))
); );
let res = test_parse!(parse_stmt, r#"REMOVE TOKEN foo ON SCOPE bar"#).unwrap(); let res = test_parse!(parse_stmt, r#"REMOVE ACCESS foo ON DATABASE"#).unwrap();
assert_eq!( assert_eq!(
res, res,
Statement::Remove(RemoveStatement::Token(RemoveTokenStatement { Statement::Remove(RemoveStatement::Access(RemoveAccessStatement {
name: Ident("foo".to_owned()),
base: Base::Sc(Ident("bar".to_owned())),
if_exists: false,
}))
);
let res = test_parse!(parse_stmt, r#"REMOVE SCOPE foo"#).unwrap();
assert_eq!(
res,
Statement::Remove(RemoveStatement::Scope(RemoveScopeStatement {
name: Ident("foo".to_owned()), name: Ident("foo".to_owned()),
base: Base::Db,
if_exists: false, if_exists: false,
})) }))
); );

View file

@ -1,5 +1,6 @@
use crate::{ use crate::{
sql::{ sql::{
access_type::{AccessType, JwtAccess, JwtAccessVerify, JwtAccessVerifyKey, RecordAccess},
block::Entry, block::Entry,
changefeed::ChangeFeed, changefeed::ChangeFeed,
filter::Filter, filter::Filter,
@ -8,13 +9,13 @@ use crate::{
statements::{ statements::{
analyze::AnalyzeStatement, show::ShowSince, show::ShowStatement, sleep::SleepStatement, analyze::AnalyzeStatement, show::ShowSince, show::ShowStatement, sleep::SleepStatement,
BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement, BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement,
CreateStatement, DefineAnalyzerStatement, DefineDatabaseStatement, CreateStatement, DefineAccessStatement, DefineAnalyzerStatement,
DefineEventStatement, DefineFieldStatement, DefineFunctionStatement, DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement,
DefineIndexStatement, DefineNamespaceStatement, DefineParamStatement, DefineStatement, DefineFunctionStatement, DefineIndexStatement, DefineNamespaceStatement,
DefineTableStatement, DefineTokenStatement, DeleteStatement, ForeachStatement, DefineParamStatement, DefineStatement, DefineTableStatement, DeleteStatement,
IfelseStatement, InfoStatement, InsertStatement, KillStatement, OutputStatement, ForeachStatement, IfelseStatement, InfoStatement, InsertStatement, KillStatement,
RelateStatement, RemoveFieldStatement, RemoveFunctionStatement, RemoveStatement, OutputStatement, RelateStatement, RemoveFieldStatement, RemoveFunctionStatement,
SelectStatement, SetStatement, ThrowStatement, UpdateStatement, RemoveStatement, SelectStatement, SetStatement, ThrowStatement, UpdateStatement,
}, },
tokenizer::Tokenizer, tokenizer::Tokenizer,
Algorithm, Array, Base, Block, Cond, Data, Datetime, Dir, Duration, Edges, Explain, Algorithm, Array, Base, Block, Cond, Data, Datetime, Dir, Duration, Edges, Explain,
@ -46,7 +47,7 @@ static SOURCE: &str = r#"
DEFINE FUNCTION fn::foo::bar($a: number, $b: array<bool,3>) { DEFINE FUNCTION fn::foo::bar($a: number, $b: array<bool,3>) {
RETURN a RETURN a
} COMMENT 'test' PERMISSIONS FULL; } COMMENT 'test' PERMISSIONS FULL;
DEFINE TOKEN a ON SCOPE b TYPE EDDSA VALUE "foo" COMMENT "bar"; DEFINE ACCESS a ON DATABASE TYPE RECORD WITH JWT ALGORITHM EDDSA KEY "foo" COMMENT "bar";
DEFINE PARAM $a VALUE { a: 1, "b": 3 } PERMISSIONS WHERE null; DEFINE PARAM $a VALUE { a: 1, "b": 3 } PERMISSIONS WHERE null;
DEFINE TABLE name DROP SCHEMAFUL CHANGEFEED 1s PERMISSIONS FOR SELECT WHERE a = 1 AS SELECT foo FROM bar GROUP BY foo; DEFINE TABLE name DROP SCHEMAFUL CHANGEFEED 1s PERMISSIONS FOR SELECT WHERE a = 1 AS SELECT foo FROM bar GROUP BY foo;
DEFINE EVENT event ON TABLE table WHEN null THEN null,none; DEFINE EVENT event ON TABLE table WHEN null THEN null,none;
@ -73,7 +74,6 @@ static SOURCE: &str = r#"
IF foo { bar } ELSE IF faz { baz } ELSE { baq }; IF foo { bar } ELSE IF faz { baz } ELSE { baq };
INFO FOR ROOT; INFO FOR ROOT;
INFO FOR NAMESPACE; INFO FOR NAMESPACE;
INFO FOR SCOPE scope;
INFO FOR USER user ON namespace; INFO FOR USER user ON namespace;
SELECT bar as foo,[1,2],bar OMIT bar FROM ONLY a,1 SELECT bar as foo,[1,2],bar OMIT bar FROM ONLY a,1
WITH INDEX index,index_2 WITH INDEX index,index_2
@ -191,11 +191,21 @@ fn statements() -> Vec<Statement> {
permissions: Permission::Full, permissions: Permission::Full,
if_not_exists: false, if_not_exists: false,
})), })),
Statement::Define(DefineStatement::Token(DefineTokenStatement { Statement::Define(DefineStatement::Access(DefineAccessStatement {
name: Ident("a".to_string()), name: Ident("a".to_string()),
base: Base::Sc(Ident("b".to_string())), base: Base::Db,
kind: Algorithm::EdDSA, kind: AccessType::Record(RecordAccess {
code: "foo".to_string(), duration: Some(Duration::from_hours(1)),
signup: None,
signin: None,
jwt: JwtAccess {
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: Algorithm::EdDSA,
key: "foo".to_string(),
}),
issue: None,
},
}),
comment: Some(Strand("bar".to_string())), comment: Some(Strand("bar".to_string())),
if_not_exists: false, if_not_exists: false,
})), })),
@ -435,7 +445,6 @@ fn statements() -> Vec<Statement> {
}), }),
Statement::Info(InfoStatement::Root(false)), Statement::Info(InfoStatement::Root(false)),
Statement::Info(InfoStatement::Ns(false)), Statement::Info(InfoStatement::Ns(false)),
Statement::Info(InfoStatement::Sc(Ident("scope".to_owned()), false)),
Statement::Info(InfoStatement::User(Ident("user".to_owned()), Some(Base::Ns), false)), Statement::Info(InfoStatement::User(Ident("user".to_owned()), Some(Base::Ns), false)),
Statement::Select(SelectStatement { Statement::Select(SelectStatement {
expr: Fields( expr: Fields(

View file

@ -24,7 +24,9 @@ macro_rules! keyword {
} }
keyword! { keyword! {
Access => "ACCESS",
After => "AFTER", After => "AFTER",
Algorithm => "ALGORITHM",
All => "ALL", All => "ALL",
Analyze => "ANALYZE", Analyze => "ANALYZE",
Analyzer => "ANALYZER", Analyzer => "ANALYZER",
@ -93,6 +95,9 @@ keyword! {
Into => "INTO", Into => "INTO",
If => "IF", If => "IF",
Is => "IS", Is => "IS",
Issuer => "ISSUER",
Jwt => "JWT",
Jwks => "JWKS",
Key => "KEY", Key => "KEY",
KeepPrunedConnections => "KEEP_PRUNED_CONNECTIONS", KeepPrunedConnections => "KEEP_PRUNED_CONNECTIONS",
Kill => "KILL", Kill => "KILL",
@ -169,6 +174,7 @@ keyword! {
Unset => "UNSET", Unset => "UNSET",
Update => "UPDATE", Update => "UPDATE",
Uppercase => "UPPERCASE", Uppercase => "UPPERCASE",
Url => "URL",
Use => "USE", Use => "USE",
User => "USER", User => "USER",
Value => "VALUE", Value => "VALUE",

View file

@ -365,8 +365,8 @@ impl TokenKind {
) )
} }
fn algorithm_as_str(algo: Algorithm) -> &'static str { fn algorithm_as_str(alg: Algorithm) -> &'static str {
match algo { match alg {
Algorithm::EdDSA => "EDDSA", Algorithm::EdDSA => "EDDSA",
Algorithm::Es256 => "ES256", Algorithm::Es256 => "ES256",
Algorithm::Es384 => "ES384", Algorithm::Es384 => "ES384",
@ -380,7 +380,6 @@ impl TokenKind {
Algorithm::Rs256 => "RS256", Algorithm::Rs256 => "RS256",
Algorithm::Rs384 => "RS384", Algorithm::Rs384 => "RS384",
Algorithm::Rs512 => "RS512", Algorithm::Rs512 => "RS512",
Algorithm::Jwks => "JWKS",
} }
} }

151
lib/fuzz/Cargo.lock generated
View file

@ -227,12 +227,6 @@ dependencies = [
"critical-section", "critical-section",
] ]
[[package]]
name = "atomic-waker"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.2.0" version = "1.2.0"
@ -531,6 +525,33 @@ dependencies = [
"windows-targets 0.52.4", "windows-targets 0.52.4",
] ]
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]] [[package]]
name = "cipher" name = "cipher"
version = "0.4.4" version = "0.4.4"
@ -732,9 +753,9 @@ dependencies = [
[[package]] [[package]]
name = "echodb" name = "echodb"
version = "0.4.0" version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "312221c0bb46e82cd250c818404ef9dce769a4d5a62915c0249b577762eec34a" checksum = "1ac31e38aeac770dd01b9d6c9ab2a6d7f025815f71105911cf6de073a5db8ee1"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"imbl", "imbl",
@ -1063,6 +1084,16 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "half"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]] [[package]]
name = "hash32" name = "hash32"
version = "0.2.1" version = "0.2.1"
@ -1622,6 +1653,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "path-clean" name = "path-clean"
version = "1.0.1" version = "1.0.1"
@ -1685,6 +1722,40 @@ dependencies = [
"rustc_version", "rustc_version",
] ]
[[package]]
name = "phf"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
dependencies = [
"phf_macros",
"phf_shared 0.11.2",
]
[[package]]
name = "phf_generator"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
dependencies = [
"phf_shared 0.11.2",
"rand",
]
[[package]]
name = "phf_macros"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
dependencies = [
"phf_generator",
"phf_shared 0.11.2",
"proc-macro2",
"quote",
"syn 2.0.58",
"unicase",
]
[[package]] [[package]]
name = "phf_shared" name = "phf_shared"
version = "0.10.0" version = "0.10.0"
@ -1694,6 +1765,16 @@ dependencies = [
"siphasher", "siphasher",
] ]
[[package]]
name = "phf_shared"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
dependencies = [
"siphasher",
"unicase",
]
[[package]] [[package]]
name = "pico-args" name = "pico-args"
version = "0.5.0" version = "0.5.0"
@ -1927,14 +2008,9 @@ dependencies = [
[[package]] [[package]]
name = "reblessive" name = "reblessive"
version = "0.3.2" version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb88832db7cfbac349e0276a84d4fbbcdb9cfe8069affbc765d8fd012265ba45" checksum = "4149deda5bd21e0f6ccaa2f907cd542541521dead5861bc51bebdf2af4acaf2a"
dependencies = [
"atomic-waker",
"pin-project-lite",
"pin-utils",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
@ -2019,9 +2095,9 @@ dependencies = [
[[package]] [[package]]
name = "revision" name = "revision"
version = "0.5.0" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87eb86913082f8976b06d07a59f17df9120e6f38b882cf3fc5a45b4499e224b6" checksum = "588784c1d9453cfd2ce1b7aff06c903513677cf0e63779a0a3085ee8a44f5b17"
dependencies = [ dependencies = [
"bincode", "bincode",
"chrono", "chrono",
@ -2037,9 +2113,9 @@ dependencies = [
[[package]] [[package]]
name = "revision-derive" name = "revision-derive"
version = "0.5.0" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf996fc5f61f1dbec35799b5c00c6dda12e8862e8cb782ed24e10d0292e60ed3" checksum = "854ff0b6794d4e0aab5e4486870941caefe9f258e63cad2f21b49a6302377c85"
dependencies = [ dependencies = [
"darling", "darling",
"proc-macro-error", "proc-macro-error",
@ -2107,6 +2183,27 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "rmp"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rmpv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9"
dependencies = [
"num-traits",
"rmp",
]
[[package]] [[package]]
name = "roaring" name = "roaring"
version = "0.10.3" version = "0.10.3"
@ -2522,7 +2619,7 @@ dependencies = [
"new_debug_unreachable", "new_debug_unreachable",
"once_cell", "once_cell",
"parking_lot", "parking_lot",
"phf_shared", "phf_shared 0.10.0",
"precomputed-hash", "precomputed-hash",
] ]
@ -2554,6 +2651,7 @@ dependencies = [
"once_cell", "once_cell",
"path-clean", "path-clean",
"pharos", "pharos",
"reblessive",
"revision", "revision",
"ring 0.17.8", "ring 0.17.8",
"rust_decimal", "rust_decimal",
@ -2588,6 +2686,7 @@ dependencies = [
"bytes", "bytes",
"cedar-policy", "cedar-policy",
"chrono", "chrono",
"ciborium",
"deunicode", "deunicode",
"dmp", "dmp",
"echodb", "echodb",
@ -2607,6 +2706,7 @@ dependencies = [
"once_cell", "once_cell",
"pbkdf2", "pbkdf2",
"pharos", "pharos",
"phf",
"pin-project-lite", "pin-project-lite",
"quick_cache", "quick_cache",
"radix_trie", "radix_trie",
@ -2616,6 +2716,7 @@ dependencies = [
"regex-syntax", "regex-syntax",
"revision", "revision",
"ring 0.17.8", "ring 0.17.8",
"rmpv",
"roaring", "roaring",
"rust-stemmers", "rust-stemmers",
"rust_decimal", "rust_decimal",
@ -2634,6 +2735,7 @@ dependencies = [
"tracing", "tracing",
"trice", "trice",
"ulid", "ulid",
"unicase",
"url", "url",
"uuid", "uuid",
"wasm-bindgen-futures", "wasm-bindgen-futures",
@ -2918,6 +3020,15 @@ dependencies = [
"web-time", "web-time",
] ]
[[package]]
name = "unicase"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
dependencies = [
"version_check",
]
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.15" version = "0.3.15"

View file

@ -1,4 +1,6 @@
"ACCESS"
"AFTER" "AFTER"
"ALGORITHM"
"ALL" "ALL"
"ALLINSIDE" "ALLINSIDE"
"AND" "AND"
@ -61,6 +63,9 @@
"INTERSECTS" "INTERSECTS"
"INTO" "INTO"
"IS" "IS"
"JWKS"
"JWT"
"KEY"
"KILL" "KILL"
"KV" "KV"
"LET" "LET"
@ -89,15 +94,14 @@
"PATH" "PATH"
"PERMISSIONS" "PERMISSIONS"
"PI" "PI"
"RECORD"
"RELATE" "RELATE"
"REMOVE" "REMOVE"
"REPLACE" "REPLACE"
"RETURN" "RETURN"
"SC"
"SCHEMAFUL" "SCHEMAFUL"
"SCHEMAFULL" "SCHEMAFULL"
"SCHEMALESS" "SCHEMALESS"
"SCOPE"
"SELECT" "SELECT"
"SESSION" "SESSION"
"SET" "SET"
@ -116,14 +120,13 @@
"TB" "TB"
"THEN" "THEN"
"TIMEOUT" "TIMEOUT"
"TK"
"TOKEN"
"TRANSACTION" "TRANSACTION"
"TRUE" "TRUE"
"TYPE" "TYPE"
"UNIQUE" "UNIQUE"
"UPDATE" "UPDATE"
"UPPERCASE" "UPPERCASE"
"URL"
"USE" "USE"
"USER" "USER"
"VALUE" "VALUE"
@ -277,12 +280,12 @@
"search::offsets(" "search::offsets("
"session" "session"
"session::" "session::"
"session::ac("
"session::db(" "session::db("
"session::id(" "session::id("
"session::ip(" "session::ip("
"session::ns(" "session::ns("
"session::origin(" "session::origin("
"session::sc"
# Sleep is just going to slow the fuzzer down # Sleep is just going to slow the fuzzer down
# "sleep(" # "sleep("
"string" "string"

View file

@ -1,4 +1,5 @@
"AFTER" "AFTER"
"ALGORITHM"
"ALL" "ALL"
"ALLINSIDE" "ALLINSIDE"
"AND" "AND"
@ -61,6 +62,9 @@
"INTERSECTS" "INTERSECTS"
"INTO" "INTO"
"IS" "IS"
"JWKS"
"JWT"
"KEY"
"KILL" "KILL"
"KV" "KV"
"LET" "LET"
@ -89,15 +93,14 @@
"PATH" "PATH"
"PERMISSIONS" "PERMISSIONS"
"PI" "PI"
"RECORD"
"RELATE" "RELATE"
"REMOVE" "REMOVE"
"REPLACE" "REPLACE"
"RETURN" "RETURN"
"SC"
"SCHEMAFUL" "SCHEMAFUL"
"SCHEMAFULL" "SCHEMAFULL"
"SCHEMALESS" "SCHEMALESS"
"SCOPE"
"SELECT" "SELECT"
"SESSION" "SESSION"
"SET" "SET"
@ -116,14 +119,13 @@
"TB" "TB"
"THEN" "THEN"
"TIMEOUT" "TIMEOUT"
"TK"
"TOKEN"
"TRANSACTION" "TRANSACTION"
"TRUE" "TRUE"
"TYPE" "TYPE"
"UNIQUE" "UNIQUE"
"UPDATE" "UPDATE"
"UPPERCASE" "UPPERCASE"
"URL"
"USE" "USE"
"USER" "USER"
"VALUE" "VALUE"
@ -277,12 +279,12 @@
"search::offsets(" "search::offsets("
"session" "session"
"session::" "session::"
"session::ac("
"session::db(" "session::db("
"session::id(" "session::id("
"session::ip(" "session::ip("
"session::ns(" "session::ns("
"session::origin(" "session::origin("
"session::sc"
"sleep(" "sleep("
"string" "string"
"string::concat(" "string::concat("

View file

@ -393,7 +393,7 @@ where
} }
} }
/// Signs up a user to a specific authentication scope /// Signs up a user with a specific record access method
/// ///
/// # Examples /// # Examples
/// ///
@ -401,7 +401,7 @@ where
/// use serde::Serialize; /// use serde::Serialize;
/// use surrealdb::sql; /// use surrealdb::sql;
/// use surrealdb::opt::auth::Root; /// use surrealdb::opt::auth::Root;
/// use surrealdb::opt::auth::Scope; /// use surrealdb::opt::auth::Record;
/// ///
/// #[derive(Debug, Serialize)] /// #[derive(Debug, Serialize)]
/// struct AuthParams { /// struct AuthParams {
@ -423,19 +423,19 @@ where
/// // Select the namespace/database to use /// // Select the namespace/database to use
/// db.use_ns("namespace").use_db("database").await?; /// db.use_ns("namespace").use_db("database").await?;
/// ///
/// // Define the scope /// // Define the user record access
/// let sql = r#" /// let sql = r#"
/// DEFINE SCOPE user_scope SESSION 24h /// DEFINE ACCESS user_access ON DATABASE TYPE RECORD DURATION 24h
/// SIGNUP ( CREATE user SET email = $email, password = crypto::argon2::generate($password) ) /// SIGNUP ( CREATE user SET email = $email, password = crypto::argon2::generate($password) )
/// SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) ) /// SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) )
/// "#; /// "#;
/// db.query(sql).await?.check()?; /// db.query(sql).await?.check()?;
/// ///
/// // Sign a user up /// // Sign a user up
/// db.signup(Scope { /// db.signup(Record {
/// namespace: "namespace", /// namespace: "namespace",
/// database: "database", /// database: "database",
/// scope: "user_scope", /// access: "user_access",
/// params: AuthParams { /// params: AuthParams {
/// email: "john.doe@example.com".into(), /// email: "john.doe@example.com".into(),
/// password: "password123".into(), /// password: "password123".into(),
@ -453,7 +453,7 @@ where
} }
} }
/// Signs this connection in to a specific authentication scope /// Signs this connection in to a specific authentication level
/// ///
/// # Examples /// # Examples
/// ///
@ -530,12 +530,12 @@ where
/// # } /// # }
/// ``` /// ```
/// ///
/// Scope signin /// Record signin
/// ///
/// ```no_run /// ```no_run
/// use serde::Serialize; /// use serde::Serialize;
/// use surrealdb::opt::auth::Root; /// use surrealdb::opt::auth::Root;
/// use surrealdb::opt::auth::Scope; /// use surrealdb::opt::auth::Record;
/// ///
/// #[derive(Debug, Serialize)] /// #[derive(Debug, Serialize)]
/// struct AuthParams { /// struct AuthParams {
@ -551,10 +551,10 @@ where
/// db.use_ns("namespace").use_db("database").await?; /// db.use_ns("namespace").use_db("database").await?;
/// ///
/// // Sign a user in /// // Sign a user in
/// db.signin(Scope { /// db.signin(Record {
/// namespace: "namespace", /// namespace: "namespace",
/// database: "database", /// database: "database",
/// scope: "user_scope", /// access: "user_access",
/// params: AuthParams { /// params: AuthParams {
/// email: "john.doe@example.com".into(), /// email: "john.doe@example.com".into(),
/// password: "password123".into(), /// password: "password123".into(),

View file

@ -9,8 +9,8 @@ use crate::api::method::tests::types::AuthParams;
use crate::api::opt::auth::Database; use crate::api::opt::auth::Database;
use crate::api::opt::auth::Jwt; use crate::api::opt::auth::Jwt;
use crate::api::opt::auth::Namespace; use crate::api::opt::auth::Namespace;
use crate::api::opt::auth::Record;
use crate::api::opt::auth::Root; use crate::api::opt::auth::Root;
use crate::api::opt::auth::Scope;
use crate::api::opt::PatchOp; use crate::api::opt::PatchOp;
use crate::api::Response as QueryResponse; use crate::api::Response as QueryResponse;
use crate::api::Surreal; use crate::api::Surreal;
@ -42,10 +42,10 @@ async fn api() {
// signup // signup
let _: Jwt = DB let _: Jwt = DB
.signup(Scope { .signup(Record {
namespace: "test-ns", namespace: "test-ns",
database: "test-db", database: "test-db",
scope: "scope", access: "access",
params: AuthParams {}, params: AuthParams {},
}) })
.await .await
@ -77,10 +77,10 @@ async fn api() {
.await .await
.unwrap(); .unwrap();
let _: Jwt = DB let _: Jwt = DB
.signin(Scope { .signin(Record {
namespace: "test-ns", namespace: "test-ns",
database: "test-db", database: "test-db",
scope: "scope", access: "access",
params: AuthParams {}, params: AuthParams {},
}) })
.await .await

View file

@ -63,24 +63,24 @@ pub struct Database<'a> {
impl Credentials<Signin, Jwt> for Database<'_> {} impl Credentials<Signin, Jwt> for Database<'_> {}
/// Credentials for the scope user /// Credentials for the record user
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct Scope<'a, P> { pub struct Record<'a, P> {
/// The namespace the user has access to /// The namespace the user has access to
#[serde(rename = "ns")] #[serde(rename = "ns")]
pub namespace: &'a str, pub namespace: &'a str,
/// The database the user has access to /// The database the user has access to
#[serde(rename = "db")] #[serde(rename = "db")]
pub database: &'a str, pub database: &'a str,
/// The scope to use for signin and signup /// The access method to use for signin and signup
#[serde(rename = "sc")] #[serde(rename = "ac")]
pub scope: &'a str, pub access: &'a str,
/// The additional params to use /// The additional params to use
#[serde(flatten)] #[serde(flatten)]
pub params: P, pub params: P,
} }
impl<T, P> Credentials<T, Jwt> for Scope<'_, P> where P: Serialize {} impl<T, P> Credentials<T, Jwt> for Record<'_, P> where P: Serialize {}
/// A JSON Web Token for authenticating with the server. /// A JSON Web Token for authenticating with the server.
/// ///

View file

@ -18,8 +18,8 @@ mod api_integration {
use surrealdb::opt::auth::Database; use surrealdb::opt::auth::Database;
use surrealdb::opt::auth::Jwt; use surrealdb::opt::auth::Jwt;
use surrealdb::opt::auth::Namespace; use surrealdb::opt::auth::Namespace;
use surrealdb::opt::auth::Record as RecordAccess;
use surrealdb::opt::auth::Root; use surrealdb::opt::auth::Root;
use surrealdb::opt::auth::Scope;
use surrealdb::opt::Config; use surrealdb::opt::Config;
use surrealdb::opt::PatchOp; use surrealdb::opt::PatchOp;
use surrealdb::opt::Resource; use surrealdb::opt::Resource;

View file

@ -52,14 +52,14 @@ async fn invalidate() {
} }
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn signup_scope() { async fn signup_record() {
let (permit, db) = new_db().await; let (permit, db) = new_db().await;
let database = Ulid::new().to_string(); let database = Ulid::new().to_string();
db.use_ns(NS).use_db(&database).await.unwrap(); db.use_ns(NS).use_db(&database).await.unwrap();
let scope = Ulid::new().to_string(); let access = Ulid::new().to_string();
let sql = format!( let sql = format!(
" "
DEFINE SCOPE `{scope}` SESSION 1s DEFINE ACCESS `{access}` ON DB TYPE RECORD DURATION 1s
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
" "
@ -67,10 +67,10 @@ async fn signup_scope() {
let response = db.query(sql).await.unwrap(); let response = db.query(sql).await.unwrap();
drop(permit); drop(permit);
response.check().unwrap(); response.check().unwrap();
db.signup(Scope { db.signup(RecordAccess {
namespace: NS, namespace: NS,
database: &database, database: &database,
scope: &scope, access: &access,
params: AuthParams { params: AuthParams {
email: "john.doe@example.com", email: "john.doe@example.com",
pass: "password123", pass: "password123",
@ -121,16 +121,16 @@ async fn signin_db() {
} }
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn signin_scope() { async fn signin_record() {
let (permit, db) = new_db().await; let (permit, db) = new_db().await;
let database = Ulid::new().to_string(); let database = Ulid::new().to_string();
db.use_ns(NS).use_db(&database).await.unwrap(); db.use_ns(NS).use_db(&database).await.unwrap();
let scope = Ulid::new().to_string(); let access = Ulid::new().to_string();
let email = format!("{scope}@example.com"); let email = format!("{access}@example.com");
let pass = "password123"; let pass = "password123";
let sql = format!( let sql = format!(
" "
DEFINE SCOPE `{scope}` SESSION 1s DEFINE ACCESS `{access}` ON DB TYPE RECORD DURATION 1s
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
" "
@ -138,10 +138,10 @@ async fn signin_scope() {
let response = db.query(sql).await.unwrap(); let response = db.query(sql).await.unwrap();
drop(permit); drop(permit);
response.check().unwrap(); response.check().unwrap();
db.signup(Scope { db.signup(RecordAccess {
namespace: NS, namespace: NS,
database: &database, database: &database,
scope: &scope, access: &access,
params: AuthParams { params: AuthParams {
pass, pass,
email: &email, email: &email,
@ -149,10 +149,10 @@ async fn signin_scope() {
}) })
.await .await
.unwrap(); .unwrap();
db.signin(Scope { db.signin(RecordAccess {
namespace: NS, namespace: NS,
database: &database, database: &database,
scope: &scope, access: &access,
params: AuthParams { params: AuthParams {
pass, pass,
email: &email, email: &email,
@ -163,16 +163,16 @@ async fn signin_scope() {
} }
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn scope_throws_error() { async fn record_access_throws_error() {
let (permit, db) = new_db().await; let (permit, db) = new_db().await;
let database = Ulid::new().to_string(); let database = Ulid::new().to_string();
db.use_ns(NS).use_db(&database).await.unwrap(); db.use_ns(NS).use_db(&database).await.unwrap();
let scope = Ulid::new().to_string(); let access = Ulid::new().to_string();
let email = format!("{scope}@example.com"); let email = format!("{access}@example.com");
let pass = "password123"; let pass = "password123";
let sql = format!( let sql = format!(
" "
DEFINE SCOPE `{scope}` SESSION 1s DEFINE ACCESS `{access}` ON DB TYPE RECORD DURATION 1s
SIGNUP {{ THROW 'signup_thrown_error' }} SIGNUP {{ THROW 'signup_thrown_error' }}
SIGNIN {{ THROW 'signin_thrown_error' }} SIGNIN {{ THROW 'signin_thrown_error' }}
" "
@ -182,10 +182,10 @@ async fn scope_throws_error() {
response.check().unwrap(); response.check().unwrap();
match db match db
.signup(Scope { .signup(RecordAccess {
namespace: NS, namespace: NS,
database: &database, database: &database,
scope: &scope, access: &access,
params: AuthParams { params: AuthParams {
pass, pass,
email: &email, email: &email,
@ -203,10 +203,10 @@ async fn scope_throws_error() {
}; };
match db match db
.signin(Scope { .signin(RecordAccess {
namespace: NS, namespace: NS,
database: &database, database: &database,
scope: &scope, access: &access,
params: AuthParams { params: AuthParams {
pass, pass,
email: &email, email: &email,
@ -225,16 +225,16 @@ async fn scope_throws_error() {
} }
#[test_log::test(tokio::test)] #[test_log::test(tokio::test)]
async fn scope_invalid_query() { async fn record_access_invalid_query() {
let (permit, db) = new_db().await; let (permit, db) = new_db().await;
let database = Ulid::new().to_string(); let database = Ulid::new().to_string();
db.use_ns(NS).use_db(&database).await.unwrap(); db.use_ns(NS).use_db(&database).await.unwrap();
let scope = Ulid::new().to_string(); let access = Ulid::new().to_string();
let email = format!("{scope}@example.com"); let email = format!("{access}@example.com");
let pass = "password123"; let pass = "password123";
let sql = format!( let sql = format!(
" "
DEFINE SCOPE `{scope}` SESSION 1s DEFINE ACCESS `{access}` ON DB TYPE RECORD DURATION 1s
SIGNUP {{ SELECT * FROM ONLY [1, 2] }} SIGNUP {{ SELECT * FROM ONLY [1, 2] }}
SIGNIN {{ SELECT * FROM ONLY [1, 2] }} SIGNIN {{ SELECT * FROM ONLY [1, 2] }}
" "
@ -244,10 +244,10 @@ async fn scope_invalid_query() {
response.check().unwrap(); response.check().unwrap();
match db match db
.signup(Scope { .signup(RecordAccess {
namespace: NS, namespace: NS,
database: &database, database: &database,
scope: &scope, access: &access,
params: AuthParams { params: AuthParams {
pass, pass,
email: &email, email: &email,
@ -255,9 +255,12 @@ async fn scope_invalid_query() {
}) })
.await .await
{ {
Err(Error::Db(surrealdb::err::Error::SignupQueryFailed)) => (), Err(Error::Db(surrealdb::err::Error::AccessRecordSignupQueryFailed)) => (),
Err(Error::Api(surrealdb::error::Api::Query(e))) => { Err(Error::Api(surrealdb::error::Api::Query(e))) => {
assert_eq!(e, "There was a problem with the database: The signup query failed") assert_eq!(
e,
"There was a problem with the database: The record access signup query failed"
)
} }
Err(Error::Api(surrealdb::error::Api::Http(e))) => assert_eq!( Err(Error::Api(surrealdb::error::Api::Http(e))) => assert_eq!(
e, e,
@ -267,10 +270,10 @@ async fn scope_invalid_query() {
}; };
match db match db
.signin(Scope { .signin(RecordAccess {
namespace: NS, namespace: NS,
database: &database, database: &database,
scope: &scope, access: &access,
params: AuthParams { params: AuthParams {
pass, pass,
email: &email, email: &email,
@ -278,9 +281,12 @@ async fn scope_invalid_query() {
}) })
.await .await
{ {
Err(Error::Db(surrealdb::err::Error::SigninQueryFailed)) => (), Err(Error::Db(surrealdb::err::Error::AccessRecordSigninQueryFailed)) => (),
Err(Error::Api(surrealdb::error::Api::Query(e))) => { Err(Error::Api(surrealdb::error::Api::Query(e))) => {
assert_eq!(e, "There was a problem with the database: The signin query failed") assert_eq!(
e,
"There was a problem with the database: The record access signin query failed"
)
} }
Err(Error::Api(surrealdb::error::Api::Http(e))) => assert_eq!( Err(Error::Api(surrealdb::error::Api::Http(e))) => assert_eq!(
e, e,

View file

@ -236,7 +236,7 @@ async fn create_or_insert_with_permissions() -> Result<(), Error> {
CREATE demo SET id = demo:one; CREATE demo SET id = demo:one;
INSERT INTO demo (id) VALUES (demo:two); INSERT INTO demo (id) VALUES (demo:two);
"; ";
let ses = Session::for_scope("test", "test", "test", Thing::from(("user", "test")).into()); let ses = Session::for_record("test", "test", "test", Thing::from(("user", "test")).into());
let res = &mut dbs.execute(sql, &ses, None).await?; let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
// //

View file

@ -55,8 +55,8 @@ async fn define_statement_database() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
databases: { test: 'DEFINE DATABASE test' }, databases: { test: 'DEFINE DATABASE test' },
tokens: {},
users: {}, users: {},
}", }",
); );
@ -84,12 +84,11 @@ async fn define_statement_function() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: { test: 'DEFINE FUNCTION fn::test($first: string, $last: string) { RETURN $first + $last; } PERMISSIONS FULL' }, functions: { test: 'DEFINE FUNCTION fn::test($first: string, $last: string) { RETURN $first + $last; } PERMISSIONS FULL' },
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: {}, tables: {},
users: {}, users: {},
}", }",
@ -116,12 +115,11 @@ async fn define_statement_table_drop() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: { test: 'DEFINE TABLE test TYPE ANY DROP SCHEMALESS PERMISSIONS NONE' }, tables: { test: 'DEFINE TABLE test TYPE ANY DROP SCHEMALESS PERMISSIONS NONE' },
users: {}, users: {},
}", }",
@ -148,12 +146,11 @@ async fn define_statement_table_schemaless() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMALESS PERMISSIONS NONE' }, tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMALESS PERMISSIONS NONE' },
users: {}, users: {},
}", }",
@ -184,12 +181,11 @@ async fn define_statement_table_schemafull() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE' }, tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE' },
users: {}, users: {},
}", }",
@ -216,12 +212,11 @@ async fn define_statement_table_schemaful() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE' }, tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE' },
users: {}, users: {},
}", }",
@ -256,12 +251,11 @@ async fn define_statement_table_foreigntable() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: { tables: {
test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE', test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE',
view: 'DEFINE TABLE view TYPE ANY SCHEMALESS AS SELECT count() FROM test GROUP ALL PERMISSIONS NONE', view: 'DEFINE TABLE view TYPE ANY SCHEMALESS AS SELECT count() FROM test GROUP ALL PERMISSIONS NONE',
@ -289,12 +283,11 @@ async fn define_statement_table_foreigntable() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: { tables: {
test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE', test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE',
}, },
@ -1267,18 +1260,17 @@ async fn define_statement_analyzer() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
r#"{ r#"{
accesses: {},
analyzers: { analyzers: {
autocomplete: 'DEFINE ANALYZER autocomplete FILTERS LOWERCASE,EDGENGRAM(2,10)', autocomplete: 'DEFINE ANALYZER autocomplete FILTERS LOWERCASE,EDGENGRAM(2,10)',
english: 'DEFINE ANALYZER english TOKENIZERS BLANK,CLASS FILTERS LOWERCASE,SNOWBALL(ENGLISH)', english: 'DEFINE ANALYZER english TOKENIZERS BLANK,CLASS FILTERS LOWERCASE,SNOWBALL(ENGLISH)',
htmlAnalyzer: 'DEFINE ANALYZER htmlAnalyzer FUNCTION fn::stripHtml TOKENIZERS BLANK,CLASS' htmlAnalyzer: 'DEFINE ANALYZER htmlAnalyzer FUNCTION fn::stripHtml TOKENIZERS BLANK,CLASS'
}, },
tokens: {},
functions: { functions: {
stripHtml: "DEFINE FUNCTION fn::stripHtml($html: string) { RETURN string::replace($html, /<[^>]*>/, ''); } PERMISSIONS FULL" stripHtml: "DEFINE FUNCTION fn::stripHtml($html: string) { RETURN string::replace($html, /<[^>]*>/, ''); } PERMISSIONS FULL"
}, },
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: {}, tables: {},
users: {}, users: {},
}"#, }"#,
@ -1557,8 +1549,8 @@ async fn permissions_checks_define_db() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ databases: { DB: 'DEFINE DATABASE DB' }, tokens: { }, users: { } }"], vec!["{ accesses: { }, databases: { DB: 'DEFINE DATABASE DB' }, users: { } }"],
vec!["{ databases: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, databases: { }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [
@ -1599,8 +1591,8 @@ async fn permissions_checks_define_function() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { greet: \"DEFINE FUNCTION fn::greet() { RETURN 'Hello'; } PERMISSIONS FULL\" }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { greet: \"DEFINE FUNCTION fn::greet() { RETURN 'Hello'; } PERMISSIONS FULL\" }, models: { }, params: { }, tables: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"] vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"]
]; ];
let test_cases = [ let test_cases = [
@ -1641,8 +1633,8 @@ async fn permissions_checks_define_analyzer() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { analyzer: 'DEFINE ANALYZER analyzer TOKENIZERS BLANK' }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { analyzer: 'DEFINE ANALYZER analyzer TOKENIZERS BLANK' }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"] vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"]
]; ];
let test_cases = [ let test_cases = [
@ -1674,17 +1666,17 @@ async fn permissions_checks_define_analyzer() {
} }
#[tokio::test] #[tokio::test]
async fn permissions_checks_define_token_ns() { async fn permissions_checks_define_access_ns() {
let scenario = HashMap::from([ let scenario = HashMap::from([
("prepare", ""), ("prepare", ""),
("test", "DEFINE TOKEN token ON NS TYPE HS512 VALUE 'secret'"), ("test", "DEFINE ACCESS access ON NS TYPE JWT ALGORITHM HS512 KEY 'secret'"),
("check", "INFO FOR NS"), ("check", "INFO FOR NS"),
]); ]);
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ databases: { }, tokens: { token: \"DEFINE TOKEN token ON NAMESPACE TYPE HS512 VALUE 'secret'\" }, users: { } }"], vec!["{ accesses: { access: \"DEFINE ACCESS access ON NAMESPACE TYPE JWT ALGORITHM HS512 KEY 'secret' WITH ISSUER KEY 'secret' DURATION 1h\" }, databases: { }, users: { } }"],
vec!["{ databases: { }, tokens: { }, users: { } }"] vec!["{ accesses: { }, databases: { }, users: { } }"]
]; ];
let test_cases = [ let test_cases = [
@ -1716,17 +1708,17 @@ async fn permissions_checks_define_token_ns() {
} }
#[tokio::test] #[tokio::test]
async fn permissions_checks_define_token_db() { async fn permissions_checks_define_access_db() {
let scenario = HashMap::from([ let scenario = HashMap::from([
("prepare", ""), ("prepare", ""),
("test", "DEFINE TOKEN token ON DB TYPE HS512 VALUE 'secret'"), ("test", "DEFINE ACCESS access ON DB TYPE JWT ALGORITHM HS512 KEY 'secret'"),
("check", "INFO FOR DB"), ("check", "INFO FOR DB"),
]); ]);
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { token: \"DEFINE TOKEN token ON DATABASE TYPE HS512 VALUE 'secret'\" }, users: { } }"], vec!["{ accesses: { access: \"DEFINE ACCESS access ON DATABASE TYPE JWT ALGORITHM HS512 KEY 'secret' WITH ISSUER KEY 'secret' DURATION 1h\" }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"] vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"]
]; ];
let test_cases = [ let test_cases = [
@ -1809,8 +1801,8 @@ async fn permissions_checks_define_user_ns() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ databases: { }, tokens: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\" } }"], vec!["{ accesses: { }, databases: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\" } }"],
vec!["{ databases: { }, tokens: { }, users: { } }"] vec!["{ accesses: { }, databases: { }, users: { } }"]
]; ];
let test_cases = [ let test_cases = [
@ -1851,8 +1843,8 @@ async fn permissions_checks_define_user_db() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER\" } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER\" } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"] vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"]
]; ];
let test_cases = [ let test_cases = [
@ -1884,28 +1876,28 @@ async fn permissions_checks_define_user_db() {
} }
#[tokio::test] #[tokio::test]
async fn permissions_checks_define_scope() { async fn permissions_checks_define_access_record() {
let scenario = HashMap::from([ let scenario = HashMap::from([
("prepare", ""), ("prepare", ""),
("test", "DEFINE SCOPE account SESSION 1h;"), ("test", "DEFINE ACCESS account ON DATABASE TYPE RECORD DURATION 1h WITH JWT ALGORITHM HS512 KEY 'secret'"),
("check", "INFO FOR DB"), ("check", "INFO FOR DB"),
]); ]);
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { account: 'DEFINE SCOPE account SESSION 1h' }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { account: \"DEFINE ACCESS account ON DATABASE TYPE RECORD DURATION 1h WITH JWT ALGORITHM HS512 KEY 'secret' WITH ISSUER KEY 'secret' DURATION 1h\" }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"] vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"]
]; ];
let test_cases = [ let test_cases = [
// Root level // Root level
((().into(), Role::Owner), ("NS", "DB"), true), ((().into(), Role::Owner), ("NS", "DB"), true),
((().into(), Role::Editor), ("NS", "DB"), true), ((().into(), Role::Editor), ("NS", "DB"), false),
((().into(), Role::Viewer), ("NS", "DB"), false), ((().into(), Role::Viewer), ("NS", "DB"), false),
// Namespace level // Namespace level
((("NS",).into(), Role::Owner), ("NS", "DB"), true), ((("NS",).into(), Role::Owner), ("NS", "DB"), true),
((("NS",).into(), Role::Owner), ("OTHER_NS", "DB"), false), ((("NS",).into(), Role::Owner), ("OTHER_NS", "DB"), false),
((("NS",).into(), Role::Editor), ("NS", "DB"), true), ((("NS",).into(), Role::Editor), ("NS", "DB"), false),
((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false), ((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false),
((("NS",).into(), Role::Viewer), ("NS", "DB"), false), ((("NS",).into(), Role::Viewer), ("NS", "DB"), false),
((("NS",).into(), Role::Viewer), ("OTHER_NS", "DB"), false), ((("NS",).into(), Role::Viewer), ("OTHER_NS", "DB"), false),
@ -1913,7 +1905,7 @@ async fn permissions_checks_define_scope() {
((("NS", "DB").into(), Role::Owner), ("NS", "DB"), true), ((("NS", "DB").into(), Role::Owner), ("NS", "DB"), true),
((("NS", "DB").into(), Role::Owner), ("NS", "OTHER_DB"), false), ((("NS", "DB").into(), Role::Owner), ("NS", "OTHER_DB"), false),
((("NS", "DB").into(), Role::Owner), ("OTHER_NS", "DB"), false), ((("NS", "DB").into(), Role::Owner), ("OTHER_NS", "DB"), false),
((("NS", "DB").into(), Role::Editor), ("NS", "DB"), true), ((("NS", "DB").into(), Role::Editor), ("NS", "DB"), false),
((("NS", "DB").into(), Role::Editor), ("NS", "OTHER_DB"), false), ((("NS", "DB").into(), Role::Editor), ("NS", "OTHER_DB"), false),
((("NS", "DB").into(), Role::Editor), ("OTHER_NS", "DB"), false), ((("NS", "DB").into(), Role::Editor), ("OTHER_NS", "DB"), false),
((("NS", "DB").into(), Role::Viewer), ("NS", "DB"), false), ((("NS", "DB").into(), Role::Viewer), ("NS", "DB"), false),
@ -1935,8 +1927,8 @@ async fn permissions_checks_define_param() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { param: \"DEFINE PARAM $param VALUE 'foo' PERMISSIONS FULL\" }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { param: \"DEFINE PARAM $param VALUE 'foo' PERMISSIONS FULL\" }, tables: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"] vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"]
]; ];
let test_cases = [ let test_cases = [
@ -1974,8 +1966,8 @@ async fn permissions_checks_define_table() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { TB: 'DEFINE TABLE TB TYPE ANY SCHEMALESS PERMISSIONS NONE' }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { TB: 'DEFINE TABLE TB TYPE ANY SCHEMALESS PERMISSIONS NONE' }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"] vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"]
]; ];
let test_cases = [ let test_cases = [
@ -2159,17 +2151,16 @@ async fn define_statement_table_permissions() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: { tables: {
default: 'DEFINE TABLE default TYPE ANY SCHEMALESS PERMISSIONS NONE', default: 'DEFINE TABLE default TYPE ANY SCHEMALESS PERMISSIONS NONE',
full: 'DEFINE TABLE full TYPE ANY SCHEMALESS PERMISSIONS FULL', full: 'DEFINE TABLE full TYPE ANY SCHEMALESS PERMISSIONS FULL',
select_full: 'DEFINE TABLE select_full TYPE ANY SCHEMALESS PERMISSIONS FOR select FULL, FOR create, update, delete NONE' select_full: 'DEFINE TABLE select_full TYPE ANY SCHEMALESS PERMISSIONS FOR select FULL, FOR create, update, delete NONE'
}, },
tokens: {},
users: {} users: {}
}", }",
); );
@ -2499,10 +2490,10 @@ async fn redefining_existing_param_with_if_not_exists_should_error() -> Result<(
} }
#[tokio::test] #[tokio::test]
async fn redefining_existing_scope_should_not_error() -> Result<(), Error> { async fn redefining_existing_access_should_not_error() -> Result<(), Error> {
let sql = " let sql = "
DEFINE SCOPE example; DEFINE ACCESS example ON DATABASE TYPE JWT ALGORITHM HS512 KEY 'secret';
DEFINE SCOPE example; DEFINE ACCESS example ON DATABASE TYPE JWT ALGORITHM HS512 KEY 'secret';
"; ";
let dbs = new_ds().await?; let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test"); let ses = Session::owner().with_ns("test").with_db("test");
@ -2519,10 +2510,10 @@ async fn redefining_existing_scope_should_not_error() -> Result<(), Error> {
} }
#[tokio::test] #[tokio::test]
async fn redefining_existing_scope_with_if_not_exists_should_error() -> Result<(), Error> { async fn redefining_existing_access_with_if_not_exists_should_error() -> Result<(), Error> {
let sql = " let sql = "
DEFINE SCOPE IF NOT EXISTS example; DEFINE ACCESS IF NOT EXISTS example ON DATABASE TYPE JWT ALGORITHM HS512 KEY 'secret';
DEFINE SCOPE IF NOT EXISTS example; DEFINE ACCESS IF NOT EXISTS example ON DATABASE TYPE JWT ALGORITHM HS512 KEY 'secret';
"; ";
let dbs = new_ds().await?; let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test"); let ses = Session::owner().with_ns("test").with_db("test");
@ -2533,7 +2524,7 @@ async fn redefining_existing_scope_with_if_not_exists_should_error() -> Result<(
assert_eq!(tmp, Value::None); assert_eq!(tmp, Value::None);
// //
let tmp = res.remove(0).result.unwrap_err(); let tmp = res.remove(0).result.unwrap_err();
assert!(matches!(tmp, Error::ScAlreadyExists { .. }),); assert!(matches!(tmp, Error::AccessDbAlreadyExists { .. }),);
// //
Ok(()) Ok(())
} }
@ -2578,46 +2569,6 @@ async fn redefining_existing_table_with_if_not_exists_should_error() -> Result<(
Ok(()) Ok(())
} }
#[tokio::test]
async fn redefining_existing_token_should_not_error() -> Result<(), Error> {
let sql = "
DEFINE TOKEN example ON SCOPE example TYPE HS512 VALUE \"example\";
DEFINE TOKEN example ON SCOPE example TYPE HS512 VALUE \"example\";
";
let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 2);
//
let tmp = res.remove(0).result?;
assert_eq!(tmp, Value::None);
//
let tmp = res.remove(0).result?;
assert_eq!(tmp, Value::None);
//
Ok(())
}
#[tokio::test]
async fn redefining_existing_token_with_if_not_exists_should_error() -> Result<(), Error> {
let sql = "
DEFINE TOKEN IF NOT EXISTS example ON SCOPE example TYPE HS512 VALUE \"example\";
DEFINE TOKEN IF NOT EXISTS example ON SCOPE example TYPE HS512 VALUE \"example\";
";
let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 2);
//
let tmp = res.remove(0).result?;
assert_eq!(tmp, Value::None);
//
let tmp = res.remove(0).result.unwrap_err();
assert!(matches!(tmp, Error::StAlreadyExists { .. }),);
//
Ok(())
}
#[tokio::test] #[tokio::test]
async fn redefining_existing_user_should_not_error() -> Result<(), Error> { async fn redefining_existing_user_should_not_error() -> Result<(), Error> {
let sql = " let sql = "
@ -2831,12 +2782,11 @@ async fn define_table_relation_redefinition_info() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: { likes: 'DEFINE TABLE likes TYPE RELATION IN person OUT person SCHEMALESS PERMISSIONS NONE' }, tables: { likes: 'DEFINE TABLE likes TYPE RELATION IN person OUT person SCHEMALESS PERMISSIONS NONE' },
users: {}, users: {},
}", }",
@ -2861,12 +2811,11 @@ async fn define_table_relation_redefinition_info() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: { likes: 'DEFINE TABLE likes TYPE RELATION IN person OUT person | thing SCHEMALESS PERMISSIONS NONE' }, tables: { likes: 'DEFINE TABLE likes TYPE RELATION IN person OUT person | thing SCHEMALESS PERMISSIONS NONE' },
users: {}, users: {},
}", }",
@ -2891,12 +2840,11 @@ async fn define_table_relation_redefinition_info() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: { likes: 'DEFINE TABLE likes TYPE RELATION IN person OUT person | thing | other SCHEMALESS PERMISSIONS NONE' }, tables: { likes: 'DEFINE TABLE likes TYPE RELATION IN person OUT person | thing | other SCHEMALESS PERMISSIONS NONE' },
users: {}, users: {},
}", }",

View file

@ -456,7 +456,7 @@ async fn delete_with_permissions() -> Result<(), Error> {
DELETE friends_with:1 RETURN BEFORE; DELETE friends_with:1 RETURN BEFORE;
DELETE friends_with:2 RETURN BEFORE; DELETE friends_with:2 RETURN BEFORE;
"; ";
let ses = Session::for_scope("test", "test", "test", Thing::from(("user", "john")).into()); let ses = Session::for_record("test", "test", "test", Thing::from(("user", "john")).into());
let res = &mut dbs.execute(sql, &ses, None).await?; let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
// //

View file

@ -842,7 +842,7 @@ async fn field_definition_edge_permissions() -> Result<(), Error> {
RELATE business:one->contact:one->business:two; RELATE business:one->contact:one->business:two;
RELATE business:two->contact:two->business:one; RELATE business:two->contact:two->business:one;
"; ";
let ses = Session::for_scope("test", "test", "test", Thing::from(("user", "one")).into()); let ses = Session::for_record("test", "test", "test", Thing::from(("user", "one")).into());
let res = &mut dbs.execute(sql, &ses, None).await?; let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 2); assert_eq!(res.len(), 2);
// //

View file

@ -24,7 +24,7 @@ pub async fn iam_run_case(
sess: &Session, sess: &Session,
should_succeed: bool, should_succeed: bool,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
// Use the same scope as the test statement, but change the Auth to run the check with full permissions // Use the session as the test statement, but change the Auth to run the check with full permissions
let mut owner_sess = sess.clone(); let mut owner_sess = sess.clone();
owner_sess.au = Arc::new(Auth::for_root(Role::Owner)); owner_sess.au = Arc::new(Auth::for_root(Role::Owner));

View file

@ -39,7 +39,7 @@ async fn info_for_ns() {
let sql = r#" let sql = r#"
DEFINE DATABASE DB; DEFINE DATABASE DB;
DEFINE USER user ON NS PASSWORD 'pass'; DEFINE USER user ON NS PASSWORD 'pass';
DEFINE TOKEN token ON NS TYPE HS512 VALUE 'secret'; DEFINE ACCESS access ON NS TYPE JWT ALGORITHM HS512 KEY 'secret';
INFO FOR NS INFO FOR NS
"#; "#;
let dbs = new_ds().await.unwrap(); let dbs = new_ds().await.unwrap();
@ -52,7 +52,7 @@ async fn info_for_ns() {
assert!(out.is_ok(), "Unexpected error: {:?}", out); assert!(out.is_ok(), "Unexpected error: {:?}", out);
let output_regex = Regex::new( let output_regex = Regex::new(
r"\{ databases: \{ DB: .* \}, tokens: \{ token: .* \}, users: \{ user: .* \} \}", r"\{ accesses: \{ access: .* \}, databases: \{ DB: .* \}, users: \{ user: .* \} \}",
) )
.unwrap(); .unwrap();
let out_str = out.unwrap().to_string(); let out_str = out.unwrap().to_string();
@ -68,9 +68,9 @@ async fn info_for_ns() {
async fn info_for_db() { async fn info_for_db() {
let sql = r#" let sql = r#"
DEFINE TABLE TB; DEFINE TABLE TB;
DEFINE SCOPE account SESSION 24h; DEFINE ACCESS jwt ON DB TYPE JWT ALGORITHM HS512 KEY 'secret';
DEFINE ACCESS record ON DB TYPE RECORD DURATION 24h;
DEFINE USER user ON DB PASSWORD 'pass'; DEFINE USER user ON DB PASSWORD 'pass';
DEFINE TOKEN token ON DB TYPE HS512 VALUE 'secret';
DEFINE FUNCTION fn::greet() {RETURN "Hello";}; DEFINE FUNCTION fn::greet() {RETURN "Hello";};
DEFINE PARAM $param VALUE "foo"; DEFINE PARAM $param VALUE "foo";
DEFINE ANALYZER analyzer TOKENIZERS BLANK; DEFINE ANALYZER analyzer TOKENIZERS BLANK;
@ -85,33 +85,7 @@ async fn info_for_db() {
let out = res.pop().unwrap().output(); let out = res.pop().unwrap().output();
assert!(out.is_ok(), "Unexpected error: {:?}", out); assert!(out.is_ok(), "Unexpected error: {:?}", out);
let output_regex = Regex::new(r"\{ analyzers: \{ analyzer: .* \}, functions: \{ greet: .* \}, params: \{ param: .* \}, scopes: \{ account: .* \}, tables: \{ TB: .* \}, tokens: \{ token: .* \}, users: \{ user: .* \} \}").unwrap(); let output_regex = Regex::new(r"\{ accesses: \{ jwt: .*, record: .* \}, analyzers: \{ analyzer: .* \}, functions: \{ greet: .* \}, params: \{ param: .* \}, tables: \{ TB: .* \}, users: \{ user: .* \} \}").unwrap();
let out_str = out.unwrap().to_string();
assert!(
output_regex.is_match(&out_str),
"Output '{}' doesn't match regex '{}'",
out_str,
output_regex
);
}
#[tokio::test]
async fn info_for_scope() {
let sql = r#"
DEFINE SCOPE account SESSION 24h;
DEFINE TOKEN token ON SCOPE account TYPE HS512 VALUE 'secret';
INFO FOR SCOPE account;
"#;
let dbs = new_ds().await.unwrap();
let ses = Session::owner().with_ns("ns").with_db("db");
let mut res = dbs.execute(sql, &ses, None).await.unwrap();
assert_eq!(res.len(), 3);
let out = res.pop().unwrap().output();
assert!(out.is_ok(), "Unexpected error: {:?}", out);
let output_regex = Regex::new(r"\{ tokens: \{ token: .* \} \}").unwrap();
let out_str = out.unwrap().to_string(); let out_str = out.unwrap().to_string();
assert!( assert!(
output_regex.is_match(&out_str), output_regex.is_match(&out_str),
@ -273,8 +247,8 @@ async fn permissions_checks_info_ns() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ databases: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, databases: { }, users: { } }"],
vec!["{ databases: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, databases: { }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [
@ -312,8 +286,8 @@ async fn permissions_checks_info_db() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [
@ -344,45 +318,6 @@ async fn permissions_checks_info_db() {
assert!(res.is_ok(), "{}", res.unwrap_err()); assert!(res.is_ok(), "{}", res.unwrap_err());
} }
#[tokio::test]
async fn permissions_checks_info_scope() {
let scenario = HashMap::from([
("prepare", "DEFINE SCOPE scope SESSION 1h"),
("test", "INFO FOR SCOPE scope"),
("check", "INFO FOR SCOPE scope"),
]);
// Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [vec!["{ tokens: { } }"], vec!["{ tokens: { } }"]];
let test_cases = [
// Root level
((().into(), Role::Owner), ("NS", "DB"), true),
((().into(), Role::Editor), ("NS", "DB"), true),
((().into(), Role::Viewer), ("NS", "DB"), true),
// Namespace level
((("NS",).into(), Role::Owner), ("NS", "DB"), true),
((("NS",).into(), Role::Owner), ("OTHER_NS", "DB"), false),
((("NS",).into(), Role::Editor), ("NS", "DB"), true),
((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false),
((("NS",).into(), Role::Viewer), ("NS", "DB"), true),
((("NS",).into(), Role::Viewer), ("OTHER_NS", "DB"), false),
// Database level
((("NS", "DB").into(), Role::Owner), ("NS", "DB"), true),
((("NS", "DB").into(), Role::Owner), ("NS", "OTHER_DB"), false),
((("NS", "DB").into(), Role::Owner), ("OTHER_NS", "DB"), false),
((("NS", "DB").into(), Role::Editor), ("NS", "DB"), true),
((("NS", "DB").into(), Role::Editor), ("NS", "OTHER_DB"), false),
((("NS", "DB").into(), Role::Editor), ("OTHER_NS", "DB"), false),
((("NS", "DB").into(), Role::Viewer), ("NS", "DB"), true),
((("NS", "DB").into(), Role::Viewer), ("NS", "OTHER_DB"), false),
((("NS", "DB").into(), Role::Viewer), ("OTHER_NS", "DB"), false),
];
let res = iam_check_cases(test_cases.iter(), &scenario, check_results).await;
assert!(res.is_ok(), "{}", res.unwrap_err());
}
#[tokio::test] #[tokio::test]
async fn permissions_checks_info_table() { async fn permissions_checks_info_table() {
let scenario = HashMap::from([ let scenario = HashMap::from([

View file

@ -26,12 +26,11 @@ async fn define_global_param() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: { test: 'DEFINE PARAM $test VALUE 12345 PERMISSIONS FULL' }, params: { test: 'DEFINE PARAM $test VALUE 12345 PERMISSIONS FULL' },
scopes: {},
tables: {}, tables: {},
users: {}, users: {},
}", }",

View file

@ -36,12 +36,11 @@ async fn remove_statement_table() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: {}, tables: {},
users: {} users: {}
}", }",
@ -71,12 +70,11 @@ async fn remove_statement_analyzer() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: {}, tables: {},
users: {} users: {}
}", }",
@ -418,9 +416,9 @@ async fn should_not_error_when_remove_param_if_exists() -> Result<(), Error> {
} }
#[tokio::test] #[tokio::test]
async fn should_error_when_remove_and_scope_does_not_exist() -> Result<(), Error> { async fn should_error_when_remove_and_access_does_not_exist() -> Result<(), Error> {
let sql = " let sql = "
REMOVE SCOPE foo; REMOVE ACCESS foo ON DB;
"; ";
let dbs = new_ds().await?; let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test"); let ses = Session::owner().with_ns("test").with_db("test");
@ -428,47 +426,15 @@ async fn should_error_when_remove_and_scope_does_not_exist() -> Result<(), Error
assert_eq!(res.len(), 1); assert_eq!(res.len(), 1);
// //
let tmp = res.remove(0).result.unwrap_err(); let tmp = res.remove(0).result.unwrap_err();
assert!(matches!(tmp, Error::ScNotFound { .. }),); assert!(matches!(tmp, Error::DaNotFound { .. }),);
Ok(()) Ok(())
} }
#[tokio::test] #[tokio::test]
async fn should_not_error_when_remove_scope_if_exists() -> Result<(), Error> { async fn should_not_error_when_remove_access_if_exists() -> Result<(), Error> {
let sql = " let sql = "
REMOVE SCOPE IF EXISTS foo; REMOVE ACCESS IF EXISTS foo ON DB;
";
let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 1);
//
let tmp = res.remove(0).result?;
assert_eq!(tmp, Value::None);
Ok(())
}
#[tokio::test]
async fn should_error_when_remove_and_token_does_not_exist() -> Result<(), Error> {
let sql = "
REMOVE TOKEN foo ON NAMESPACE;
";
let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 1);
//
let tmp = res.remove(0).result.unwrap_err();
assert!(matches!(tmp, Error::NtNotFound { .. }),);
Ok(())
}
#[tokio::test]
async fn should_not_error_when_remove_token_if_exists() -> Result<(), Error> {
let sql = "
REMOVE TOKEN IF EXISTS foo ON NAMESPACE;
"; ";
let dbs = new_ds().await?; let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test"); let ses = Session::owner().with_ns("test").with_db("test");
@ -569,8 +535,8 @@ async fn permissions_checks_remove_db() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ databases: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, databases: { }, users: { } }"],
vec!["{ databases: { DB: 'DEFINE DATABASE DB' }, tokens: { }, users: { } }"], vec!["{ accesses: { }, databases: { DB: 'DEFINE DATABASE DB' }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [
@ -611,8 +577,8 @@ async fn permissions_checks_remove_function() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { greet: \"DEFINE FUNCTION fn::greet() { RETURN 'Hello'; } PERMISSIONS FULL\" }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { greet: \"DEFINE FUNCTION fn::greet() { RETURN 'Hello'; } PERMISSIONS FULL\" }, models: { }, params: { }, tables: { }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [
@ -653,8 +619,8 @@ async fn permissions_checks_remove_analyzer() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
vec!["{ analyzers: { analyzer: 'DEFINE ANALYZER analyzer TOKENIZERS BLANK' }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { analyzer: 'DEFINE ANALYZER analyzer TOKENIZERS BLANK' }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [
@ -686,17 +652,17 @@ async fn permissions_checks_remove_analyzer() {
} }
#[tokio::test] #[tokio::test]
async fn permissions_checks_remove_ns_token() { async fn permissions_checks_remove_ns_access() {
let scenario = HashMap::from([ let scenario = HashMap::from([
("prepare", "DEFINE TOKEN token ON NS TYPE HS512 VALUE 'secret'"), ("prepare", "DEFINE ACCESS access ON NS TYPE JWT ALGORITHM HS512 KEY 'secret'"),
("test", "REMOVE TOKEN token ON NS"), ("test", "REMOVE ACCESS access ON NS"),
("check", "INFO FOR NS"), ("check", "INFO FOR NS"),
]); ]);
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ databases: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, databases: { }, users: { } }"],
vec!["{ databases: { }, tokens: { token: \"DEFINE TOKEN token ON NAMESPACE TYPE HS512 VALUE 'secret'\" }, users: { } }"], vec!["{ accesses: { access: \"DEFINE ACCESS access ON NAMESPACE TYPE JWT ALGORITHM HS512 KEY 'secret' WITH ISSUER KEY 'secret' DURATION 1h\" }, databases: { }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [
@ -728,17 +694,17 @@ async fn permissions_checks_remove_ns_token() {
} }
#[tokio::test] #[tokio::test]
async fn permissions_checks_remove_db_token() { async fn permissions_checks_remove_db_access() {
let scenario = HashMap::from([ let scenario = HashMap::from([
("prepare", "DEFINE TOKEN token ON DB TYPE HS512 VALUE 'secret'"), ("prepare", "DEFINE ACCESS access ON DB TYPE JWT ALGORITHM HS512 KEY 'secret'"),
("test", "REMOVE TOKEN token ON DB"), ("test", "REMOVE ACCESS access ON DB"),
("check", "INFO FOR DB"), ("check", "INFO FOR DB"),
]); ]);
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { token: \"DEFINE TOKEN token ON DATABASE TYPE HS512 VALUE 'secret'\" }, users: { } }"], vec!["{ accesses: { access: \"DEFINE ACCESS access ON DATABASE TYPE JWT ALGORITHM HS512 KEY 'secret' WITH ISSUER KEY 'secret' DURATION 1h\" }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [
@ -821,8 +787,8 @@ async fn permissions_checks_remove_ns_user() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ databases: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, databases: { }, users: { } }"],
vec!["{ databases: { }, tokens: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\" } }"], vec!["{ accesses: { }, databases: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\" } }"],
]; ];
let test_cases = [ let test_cases = [
@ -863,8 +829,8 @@ async fn permissions_checks_remove_db_user() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER\" } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER\" } }"],
]; ];
let test_cases = [ let test_cases = [
@ -895,48 +861,6 @@ async fn permissions_checks_remove_db_user() {
assert!(res.is_ok(), "{}", res.unwrap_err()); assert!(res.is_ok(), "{}", res.unwrap_err());
} }
#[tokio::test]
async fn permissions_checks_remove_scope() {
let scenario = HashMap::from([
("prepare", "DEFINE SCOPE account SESSION 1h;"),
("test", "REMOVE SCOPE account"),
("check", "INFO FOR DB"),
]);
// Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { account: 'DEFINE SCOPE account SESSION 1h' }, tables: { }, tokens: { }, users: { } }"],
];
let test_cases = [
// Root level
((().into(), Role::Owner), ("NS", "DB"), true),
((().into(), Role::Editor), ("NS", "DB"), true),
((().into(), Role::Viewer), ("NS", "DB"), false),
// Namespace level
((("NS",).into(), Role::Owner), ("NS", "DB"), true),
((("NS",).into(), Role::Owner), ("OTHER_NS", "DB"), false),
((("NS",).into(), Role::Editor), ("NS", "DB"), true),
((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false),
((("NS",).into(), Role::Viewer), ("NS", "DB"), false),
((("NS",).into(), Role::Viewer), ("OTHER_NS", "DB"), false),
// Database level
((("NS", "DB").into(), Role::Owner), ("NS", "DB"), true),
((("NS", "DB").into(), Role::Owner), ("NS", "OTHER_DB"), false),
((("NS", "DB").into(), Role::Owner), ("OTHER_NS", "DB"), false),
((("NS", "DB").into(), Role::Editor), ("NS", "DB"), true),
((("NS", "DB").into(), Role::Editor), ("NS", "OTHER_DB"), false),
((("NS", "DB").into(), Role::Editor), ("OTHER_NS", "DB"), false),
((("NS", "DB").into(), Role::Viewer), ("NS", "DB"), false),
((("NS", "DB").into(), Role::Viewer), ("NS", "OTHER_DB"), false),
((("NS", "DB").into(), Role::Viewer), ("OTHER_NS", "DB"), false),
];
let res = iam_check_cases(test_cases.iter(), &scenario, check_results).await;
assert!(res.is_ok(), "{}", res.unwrap_err());
}
#[tokio::test] #[tokio::test]
async fn permissions_checks_remove_param() { async fn permissions_checks_remove_param() {
let scenario = HashMap::from([ let scenario = HashMap::from([
@ -947,8 +871,8 @@ async fn permissions_checks_remove_param() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { param: \"DEFINE PARAM $param VALUE 'foo' PERMISSIONS FULL\" }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { param: \"DEFINE PARAM $param VALUE 'foo' PERMISSIONS FULL\" }, tables: { }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [
@ -989,8 +913,8 @@ async fn permissions_checks_remove_table() {
// Define the expected results for the check statement when the test statement succeeded and when it failed // Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [ let check_results = [
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"],
vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { TB: 'DEFINE TABLE TB TYPE ANY SCHEMALESS PERMISSIONS NONE' }, tokens: { }, users: { } }"], vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { TB: 'DEFINE TABLE TB TYPE ANY SCHEMALESS PERMISSIONS NONE' }, users: { } }"],
]; ];
let test_cases = [ let test_cases = [

View file

@ -242,8 +242,8 @@ async fn loose_mode_all_ok() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
databases: { test: 'DEFINE DATABASE test' }, databases: { test: 'DEFINE DATABASE test' },
tokens: {},
users: {}, users: {},
}", }",
); );
@ -252,12 +252,11 @@ async fn loose_mode_all_ok() -> Result<(), Error> {
let tmp = res.remove(0).result?; let tmp = res.remove(0).result?;
let val = Value::parse( let val = Value::parse(
"{ "{
accesses: {},
analyzers: {}, analyzers: {},
tokens: {},
functions: {}, functions: {},
models: {}, models: {},
params: {}, params: {},
scopes: {},
tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMALESS PERMISSIONS NONE' }, tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMALESS PERMISSIONS NONE' },
users: {}, users: {},
}", }",

View file

@ -41,7 +41,7 @@ struct SigninParams<'a> {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
db: Option<&'a str>, db: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
sc: Option<&'a str>, ac: Option<&'a str>,
} }
enum SocketMsg { enum SocketMsg {
@ -385,7 +385,7 @@ impl Socket {
pass: &str, pass: &str,
ns: Option<&str>, ns: Option<&str>,
db: Option<&str>, db: Option<&str>,
sc: Option<&str>, ac: Option<&str>,
) -> Result<String> { ) -> Result<String> {
// Send message and receive response // Send message and receive response
let msg = self let msg = self
@ -396,7 +396,7 @@ impl Socket {
pass, pass,
ns, ns,
db, db,
sc ac
}]), }]),
) )
.await?; .await?;

View file

@ -35,11 +35,11 @@ async fn info() -> Result<(), Box<dyn std::error::Error>> {
socket.send_message_use(Some(NS), Some(DB)).await?; socket.send_message_use(Some(NS), Some(DB)).await?;
// Define a user table // Define a user table
socket.send_message_query("DEFINE TABLE user PERMISSIONS FULL").await?; socket.send_message_query("DEFINE TABLE user PERMISSIONS FULL").await?;
// Define a user scope // Define a user record access method
socket socket
.send_message_query( .send_message_query(
r#" r#"
DEFINE SCOPE scope SESSION 24h DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 24h
SIGNUP ( CREATE user SET user = $user, pass = crypto::argon2::generate($pass) ) SIGNUP ( CREATE user SET user = $user, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE user = $user AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE user = $user AND crypto::argon2::compare(pass, $pass) )
; ;
@ -57,8 +57,8 @@ async fn info() -> Result<(), Box<dyn std::error::Error>> {
"#, "#,
) )
.await?; .await?;
// Sign in as scope user // Sign in as record user
socket.send_message_signin("user", "pass", Some(NS), Some(DB), Some("scope")).await?; socket.send_message_signin("user", "pass", Some(NS), Some(DB), Some("user")).await?;
// Send INFO command // Send INFO command
let res = socket.send_request("info", json!([])).await?; let res = socket.send_request("info", json!([])).await?;
assert!(res["result"].is_object(), "result: {:?}", res); assert!(res["result"].is_object(), "result: {:?}", res);
@ -79,11 +79,11 @@ async fn signup() -> Result<(), Box<dyn std::error::Error>> {
socket.send_message_signin(USER, PASS, None, None, None).await?; socket.send_message_signin(USER, PASS, None, None, None).await?;
// Specify a namespace and database // Specify a namespace and database
socket.send_message_use(Some(NS), Some(DB)).await?; socket.send_message_use(Some(NS), Some(DB)).await?;
// Setup the scope // Define a user record access method
socket socket
.send_message_query( .send_message_query(
r#" r#"
DEFINE SCOPE scope SESSION 24h DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 24h
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
;"#, ;"#,
@ -96,7 +96,7 @@ async fn signup() -> Result<(), Box<dyn std::error::Error>> {
json!([{ json!([{
"ns": NS, "ns": NS,
"db": DB, "db": DB,
"sc": "scope", "ac": "user",
"email": "email@email.com", "email": "email@email.com",
"pass": "pass", "pass": "pass",
}]), }]),
@ -127,11 +127,11 @@ async fn signin() -> Result<(), Box<dyn std::error::Error>> {
socket.send_message_signin(USER, PASS, None, None, None).await?; socket.send_message_signin(USER, PASS, None, None, None).await?;
// Specify a namespace and database // Specify a namespace and database
socket.send_message_use(Some(NS), Some(DB)).await?; socket.send_message_use(Some(NS), Some(DB)).await?;
// Setup the scope // Define a user record access method
socket socket
.send_message_query( .send_message_query(
r#" r#"
DEFINE SCOPE scope SESSION 24h DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 24h
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
;"#, ;"#,
@ -145,7 +145,7 @@ async fn signin() -> Result<(), Box<dyn std::error::Error>> {
[{ [{
"ns": NS, "ns": NS,
"db": DB, "db": DB,
"sc": "scope", "ac": "user",
"email": "email@email.com", "email": "email@email.com",
"pass": "pass", "pass": "pass",
}] }]
@ -170,7 +170,7 @@ async fn signin() -> Result<(), Box<dyn std::error::Error>> {
[{ [{
"ns": NS, "ns": NS,
"db": DB, "db": DB,
"sc": "scope", "ac": "user",
"email": "email@email.com", "email": "email@email.com",
"pass": "pass", "pass": "pass",
}]), }]),
@ -868,11 +868,11 @@ async fn variable_auth_live_query() -> Result<(), Box<dyn std::error::Error>> {
socket_permanent.send_message_signin(USER, PASS, None, None, None).await?; socket_permanent.send_message_signin(USER, PASS, None, None, None).await?;
// Specify a namespace and database // Specify a namespace and database
socket_permanent.send_message_use(Some(NS), Some(DB)).await?; socket_permanent.send_message_use(Some(NS), Some(DB)).await?;
// Setup the scope // Define a user record access method
socket_permanent socket_permanent
.send_message_query( .send_message_query(
r#" r#"
DEFINE SCOPE scope SESSION 1s DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1s
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
;"#, ;"#,
@ -887,7 +887,7 @@ async fn variable_auth_live_query() -> Result<(), Box<dyn std::error::Error>> {
json!([{ json!([{
"ns": NS, "ns": NS,
"db": DB, "db": DB,
"sc": "scope", "ac": "user",
"email": "email@email.com", "email": "email@email.com",
"pass": "pass", "pass": "pass",
}]), }]),
@ -933,23 +933,23 @@ async fn session_expiration() {
socket.send_message_signin(USER, PASS, None, None, None).await.unwrap(); socket.send_message_signin(USER, PASS, None, None, None).await.unwrap();
// Specify a namespace and database // Specify a namespace and database
socket.send_message_use(Some(NS), Some(DB)).await.unwrap(); socket.send_message_use(Some(NS), Some(DB)).await.unwrap();
// Setup the scope // Define a user record access method
socket socket
.send_message_query( .send_message_query(
r#" r#"
DEFINE SCOPE scope SESSION 1s DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1s
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
;"#, ;"#,
) )
.await .await
.unwrap(); .unwrap();
// Create resource that requires a scope session to query // Create resource that requires a session with the access method to query
socket socket
.send_message_query( .send_message_query(
r#" r#"
DEFINE TABLE test SCHEMALESS DEFINE TABLE test SCHEMALESS
PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope" PERMISSIONS FOR select, create, update, delete WHERE $access = "user"
;"#, ;"#,
) )
.await .await
@ -970,7 +970,7 @@ async fn session_expiration() {
[{ [{
"ns": NS, "ns": NS,
"db": DB, "db": DB,
"sc": "scope", "ac": "user",
"email": "email@email.com", "email": "email@email.com",
"pass": "pass", "pass": "pass",
}] }]
@ -1012,7 +1012,7 @@ async fn session_expiration() {
[{ [{
"ns": NS, "ns": NS,
"db": DB, "db": DB,
"sc": "scope", "ac": "user",
"email": "email@email.com", "email": "email@email.com",
"pass": "pass", "pass": "pass",
}] }]
@ -1043,23 +1043,23 @@ async fn session_expiration_operations() {
let root_token = socket.send_message_signin(USER, PASS, None, None, None).await.unwrap(); let root_token = socket.send_message_signin(USER, PASS, None, None, None).await.unwrap();
// Specify a namespace and database // Specify a namespace and database
socket.send_message_use(Some(NS), Some(DB)).await.unwrap(); socket.send_message_use(Some(NS), Some(DB)).await.unwrap();
// Setup the scope // Define a user record access method
socket socket
.send_message_query( .send_message_query(
r#" r#"
DEFINE SCOPE scope SESSION 1s DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1s
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
;"#, ;"#,
) )
.await .await
.unwrap(); .unwrap();
// Create resource that requires a scope session to query // Create resource that requires a session with the access method to query
socket socket
.send_message_query( .send_message_query(
r#" r#"
DEFINE TABLE test SCHEMALESS DEFINE TABLE test SCHEMALESS
PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope" PERMISSIONS FOR select, create, update, delete WHERE $access = "user"
;"#, ;"#,
) )
.await .await
@ -1080,7 +1080,7 @@ async fn session_expiration_operations() {
[{ [{
"ns": NS, "ns": NS,
"db": DB, "db": DB,
"sc": "scope", "ac": "user",
"email": "email@email.com", "email": "email@email.com",
"pass": "pass", "pass": "pass",
}] }]
@ -1217,7 +1217,7 @@ async fn session_expiration_operations() {
json!([{ json!([{
"ns": NS, "ns": NS,
"db": DB, "db": DB,
"sc": "scope", "ac": "user",
"email": "another@email.com", "email": "another@email.com",
"pass": "pass", "pass": "pass",
}]), }]),
@ -1248,7 +1248,7 @@ async fn session_expiration_operations() {
[{ [{
"ns": NS, "ns": NS,
"db": DB, "db": DB,
"sc": "scope", "ac": "user",
"email": "another@email.com", "email": "another@email.com",
"pass": "pass", "pass": "pass",
}] }]
@ -1299,23 +1299,23 @@ async fn session_reauthentication() {
socket.send_message_query("INFO FOR ROOT").await.unwrap(); socket.send_message_query("INFO FOR ROOT").await.unwrap();
// Specify a namespace and database // Specify a namespace and database
socket.send_message_use(Some(NS), Some(DB)).await.unwrap(); socket.send_message_use(Some(NS), Some(DB)).await.unwrap();
// Setup the scope // Define a user record access method
socket socket
.send_message_query( .send_message_query(
r#" r#"
DEFINE SCOPE scope SESSION 1h DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
;"#, ;"#,
) )
.await .await
.unwrap(); .unwrap();
// Create resource that requires a scope session to query // Create resource that requires a session with the access method to query
socket socket
.send_message_query( .send_message_query(
r#" r#"
DEFINE TABLE test SCHEMALESS DEFINE TABLE test SCHEMALESS
PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope" PERMISSIONS FOR select, create, update, delete WHERE $access = "user"
;"#, ;"#,
) )
.await .await
@ -1336,7 +1336,7 @@ async fn session_reauthentication() {
[{ [{
"ns": NS, "ns": NS,
"db": DB, "db": DB,
"sc": "scope", "ac": "user",
"email": "email@email.com", "email": "email@email.com",
"pass": "pass", "pass": "pass",
}] }]
@ -1353,7 +1353,7 @@ async fn session_reauthentication() {
assert!(res["result"].is_string(), "result: {:?}", res); assert!(res["result"].is_string(), "result: {:?}", res);
let res = res["result"].as_str().unwrap(); let res = res["result"].as_str().unwrap();
assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res); assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res);
// Authenticate using the scope token // Authenticate using the token
socket.send_request("authenticate", json!([res,])).await.unwrap(); socket.send_request("authenticate", json!([res,])).await.unwrap();
// Check that we do not have root access // Check that we do not have root access
let res = socket.send_message_query("INFO FOR ROOT").await.unwrap(); let res = socket.send_message_query("INFO FOR ROOT").await.unwrap();
@ -1363,7 +1363,7 @@ async fn session_reauthentication() {
"result: {:?}", "result: {:?}",
res res
); );
// Check if the session is authenticated for the scope // Check if the session is authenticated
let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap(); let res = socket.send_message_query("SELECT VALUE working FROM test:1").await.unwrap();
assert_eq!(res[0]["result"], json!(["yes"]), "result: {:?}", res); assert_eq!(res[0]["result"], json!(["yes"]), "result: {:?}", res);
// Authenticate using the root token // Authenticate using the root token
@ -1387,23 +1387,23 @@ async fn session_reauthentication_expired() {
socket.send_message_query("INFO FOR ROOT").await.unwrap(); socket.send_message_query("INFO FOR ROOT").await.unwrap();
// Specify a namespace and database // Specify a namespace and database
socket.send_message_use(Some(NS), Some(DB)).await.unwrap(); socket.send_message_use(Some(NS), Some(DB)).await.unwrap();
// Setup the scope // Define a user record access method
socket socket
.send_message_query( .send_message_query(
r#" r#"
DEFINE SCOPE scope SESSION 1s DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1s
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
;"#, ;"#,
) )
.await .await
.unwrap(); .unwrap();
// Create resource that requires a scope session to query // Create resource that requires a session with the access method to query
socket socket
.send_message_query( .send_message_query(
r#" r#"
DEFINE TABLE test SCHEMALESS DEFINE TABLE test SCHEMALESS
PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope" PERMISSIONS FOR select, create, update, delete WHERE $access = "user"
;"#, ;"#,
) )
.await .await
@ -1424,7 +1424,7 @@ async fn session_reauthentication_expired() {
[{ [{
"ns": NS, "ns": NS,
"db": DB, "db": DB,
"sc": "scope", "ac": "user",
"email": "email@email.com", "email": "email@email.com",
"pass": "pass", "pass": "pass",
}] }]
@ -1441,7 +1441,7 @@ async fn session_reauthentication_expired() {
assert!(res["result"].is_string(), "result: {:?}", res); assert!(res["result"].is_string(), "result: {:?}", res);
let res = res["result"].as_str().unwrap(); let res = res["result"].as_str().unwrap();
assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res); assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res);
// Authenticate using the scope token, which will expire soon // Authenticate using the token, which will expire soon
socket.send_request("authenticate", json!([res,])).await.unwrap(); socket.send_request("authenticate", json!([res,])).await.unwrap();
// Wait two seconds for token to expire // Wait two seconds for token to expire
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;

View file

@ -1160,14 +1160,14 @@ mod http_integration {
.default_headers(headers) .default_headers(headers)
.build()?; .build()?;
// Create a scope // Define a record access method
{ {
let res = client let res = client
.post(format!("http://{addr}/sql")) .post(format!("http://{addr}/sql"))
.basic_auth(USER, Some(PASS)) .basic_auth(USER, Some(PASS))
.body( .body(
r#" r#"
DEFINE SCOPE scope SESSION 24h DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 24h
SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) ) SIGNUP ( CREATE user SET email = $email, pass = crypto::argon2::generate($pass) )
SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) )
; ;
@ -1178,13 +1178,13 @@ mod http_integration {
assert!(res.status().is_success(), "body: {}", res.text().await?); assert!(res.status().is_success(), "body: {}", res.text().await?);
} }
// Signup into the scope // Signup using the defined record access method
{ {
let req_body = serde_json::to_string( let req_body = serde_json::to_string(
json!({ json!({
"ns": ns, "ns": ns,
"db": db, "db": db,
"sc": "scope", "ac": "user",
"email": "email@email.com", "email": "email@email.com",
"pass": "pass", "pass": "pass",
}) })