diff --git a/core/src/cnf/mod.rs b/core/src/cnf/mod.rs index af0540cd..56f8dd31 100644 --- a/core/src/cnf/mod.rs +++ b/core/src/cnf/mod.rs @@ -21,7 +21,7 @@ pub static MAX_COMPUTATION_DEPTH: Lazy = lazy_env_parse!("SURREAL_MAX_COMPUTATION_DEPTH", u32, 120); /// 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. pub const ID_CHARS: [char; 36] = [ @@ -35,9 +35,9 @@ pub const SERVER_NAME: &str = "SurrealDB"; /// Datastore processor batch size for scan operations 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. -pub static INSECURE_FORWARD_SCOPE_ERRORS: Lazy = - lazy_env_parse!("SURREAL_INSECURE_FORWARD_SCOPE_ERRORS", bool, false); +/// Forward all signup/signin query errors to a client performing record access. Do not use in production. +pub static INSECURE_FORWARD_RECORD_ACCESS_ERRORS: Lazy = + lazy_env_parse!("SURREAL_INSECURE_FORWARD_RECORD_ACCESS_ERRORS", bool, false); #[cfg(any( feature = "kv-surrealkv", diff --git a/core/src/dbs/options.rs b/core/src/dbs/options.rs index d31c5100..464c24f7 100644 --- a/core/src/dbs/options.rs +++ b/core/src/dbs/options.rs @@ -405,9 +405,10 @@ impl Options { self.valid_for_db()?; res.on_db(self.ns(), self.db()) } - Base::Sc(sc) => { - self.valid_for_db()?; - res.on_scope(self.ns(), self.db(), sc) + // TODO(gguillemas): This variant is kept in 2.0.0 for backward compatibility. Drop in 3.0.0. + Base::Sc(_) => { + // We should not get here, the scope base is only used in parsing for backward compatibility. + return Err(Error::InvalidAuth); } }; diff --git a/core/src/dbs/session.rs b/core/src/dbs/session.rs index 53b51a87..b8baa33c 100644 --- a/core/src/dbs/session.rs +++ b/core/src/dbs/session.rs @@ -23,12 +23,12 @@ pub struct Session { pub ns: Option, /// The currently selected database pub db: Option, - /// The currently selected authentication scope - pub sc: Option, - /// The current scope authentication token + /// The current access method + pub ac: Option, + /// The current authentication token pub tk: Option, - /// The current scope authentication data - pub sd: Option, + /// The current record authentication data + pub rd: Option, /// The current expiration time of the session pub exp: Option, } @@ -46,9 +46,9 @@ impl Session { self } - /// Set the selected database for the session - pub fn with_sc(mut self, sc: &str) -> Session { - self.sc = Some(sc.to_owned()); + /// Set the selected access method for the session + pub fn with_ac(mut self, ac: &str) -> Session { + self.ac = Some(ac.to_owned()); self } @@ -84,26 +84,26 @@ impl Session { /// Convert a session into a runtime pub(crate) fn context<'a>(&self, mut ctx: Context<'a>) -> Context<'a> { - // Add scope auth data - let val: Value = self.sd.to_owned().into(); + // Add access method data + 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); - // Add scope data - let val: Value = self.sc.to_owned().into(); - ctx.add_value("scope", val); // Add token data let val: Value = self.tk.to_owned().into(); ctx.add_value("token", val); // Add session value 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(), "id".to_string() => self.id.to_owned().into(), "ip".to_string() => self.ip.to_owned().into(), "ns".to_string() => self.ns.to_owned().into(), "or".to_string() => self.or.to_owned().into(), - "sc".to_string() => self.sc.to_owned().into(), - "sd".to_string() => self.sd.to_owned().into(), + "rd".to_string() => self.rd.to_owned().into(), "tk".to_string() => self.tk.to_owned().into(), - "exp".to_string() => self.exp.to_owned().into(), }); ctx.add_value("session", val); // Output context @@ -133,19 +133,19 @@ impl Session { sess } - /// Create a scoped session for a given NS and DB - pub fn for_scope(ns: &str, db: &str, sc: &str, rid: Value) -> Session { + /// Create a record user session for a given NS and DB + pub fn for_record(ns: &str, db: &str, ac: &str, rid: Value) -> 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, ip: None, or: None, id: None, ns: Some(ns.to_owned()), db: Some(db.to_owned()), - sc: Some(sc.to_owned()), tk: None, - sd: Some(rid), + rd: Some(rid), exp: None, } } diff --git a/core/src/doc/lives.rs b/core/src/doc/lives.rs index 55a90275..a52c07d8 100644 --- a/core/src/doc/lives.rs +++ b/core/src/doc/lives.rs @@ -7,9 +7,9 @@ use crate::doc::CursorDoc; use crate::doc::Document; use crate::err::Error; use crate::fflags::FFLAGS; +use crate::sql::paths::AC; use crate::sql::paths::META; -use crate::sql::paths::SC; -use crate::sql::paths::SD; +use crate::sql::paths::RD; use crate::sql::paths::TK; use crate::sql::permission::Permission; use crate::sql::statements::LiveStatement; @@ -161,8 +161,8 @@ impl<'a> Document<'a> { // This ensures that we are using the session // of the user who created the LIVE query. let mut lqctx = Context::background(); - lqctx.add_value("auth", sess.pick(SD.as_ref())); - lqctx.add_value("scope", sess.pick(SC.as_ref())); + lqctx.add_value("access", sess.pick(AC.as_ref())); + lqctx.add_value("auth", sess.pick(RD.as_ref())); lqctx.add_value("token", sess.pick(TK.as_ref())); lqctx.add_value("session", sess); // We need to create a new options which we will diff --git a/core/src/err/mod.rs b/core/src/err/mod.rs index ba2f3ac5..dc8cb586 100644 --- a/core/src/err/mod.rs +++ b/core/src/err/mod.rs @@ -299,9 +299,9 @@ pub enum Error { value: String, }, - /// The requested namespace token does not exist - #[error("The namespace token '{value}' does not exist")] - NtNotFound { + /// The requested namespace access method does not exist + #[error("The namespace access method '{value}' does not exist")] + NaNotFound { value: String, }, @@ -317,9 +317,9 @@ pub enum Error { value: String, }, - /// The requested database token does not exist - #[error("The database token '{value}' does not exist")] - DtNotFound { + /// The requested database access method does not exist + #[error("The database access method '{value}' does not exist")] + DaNotFound { value: String, }, @@ -353,12 +353,6 @@ pub enum Error { value: String, }, - /// The requested scope does not exist - #[error("The scope '{value}' does not exist")] - ScNotFound { - value: String, - }, - // The cluster node already exists #[error("The node '{value}' already exists")] ClAlreadyExists { @@ -371,12 +365,6 @@ pub enum Error { 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 #[error("The param '${value}' does not exist")] PaNotFound { @@ -764,15 +752,6 @@ pub enum Error { #[error("The signin query failed")] 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")] MissingUserOrPass, @@ -864,12 +843,6 @@ pub enum Error { value: String, }, - /// The requested scope already exists - #[error("The scope '{value}' already exists")] - ScAlreadyExists { - value: String, - }, - /// The requested table already exists #[error("The table '{value}' already exists")] TbAlreadyExists { @@ -888,12 +861,6 @@ pub enum Error { value: String, }, - /// The requested scope token already exists - #[error("The scope token '{value}' already exists")] - StAlreadyExists { - value: String, - }, - /// The requested user already exists #[error("The user '{value}' already exists")] UserRootAlreadyExists { @@ -921,14 +888,6 @@ pub enum Error { #[error("The session has expired")] 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 #[error("A node task has failed: {0}")] NodeAgent(&'static str), @@ -940,6 +899,70 @@ pub enum Error { /// The supplied type could not be serialiazed into `sql::Value` #[error("Serialization error: {0}")] 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 for String { diff --git a/core/src/fnc/mod.rs b/core/src/fnc/mod.rs index 5cfb82c0..74dbfac2 100644 --- a/core/src/fnc/mod.rs +++ b/core/src/fnc/mod.rs @@ -230,13 +230,13 @@ pub fn synchronous(ctx: &Context<'_>, name: &str, args: Vec) -> Result rand::uuid::v7, "rand::uuid" => rand::uuid, // + "session::ac" => session::ac(ctx), "session::db" => session::db(ctx), "session::id" => session::id(ctx), "session::ip" => session::ip(ctx), "session::ns" => session::ns(ctx), "session::origin" => session::origin(ctx), - "session::sc" => session::sc(ctx), - "session::sd" => session::sd(ctx), + "session::rd" => session::rd(ctx), "session::token" => session::token(ctx), // "string::concat" => string::concat, diff --git a/core/src/fnc/script/modules/surrealdb/functions/session.rs b/core/src/fnc/script/modules/surrealdb/functions/session.rs index 5683d988..dd459954 100644 --- a/core/src/fnc/script/modules/surrealdb/functions/session.rs +++ b/core/src/fnc/script/modules/surrealdb/functions/session.rs @@ -12,7 +12,7 @@ impl_module_def!( "ip" => run, "ns" => run, "origin" => run, - "sc" => run, - "sd" => run, + "ac" => run, + "rd" => run, "token" => run ); diff --git a/core/src/fnc/session.rs b/core/src/fnc/session.rs index e18a0fee..9e5029b4 100644 --- a/core/src/fnc/session.rs +++ b/core/src/fnc/session.rs @@ -1,15 +1,19 @@ use crate::ctx::Context; use crate::err::Error; +use crate::sql::paths::AC; use crate::sql::paths::DB; use crate::sql::paths::ID; use crate::sql::paths::IP; use crate::sql::paths::NS; use crate::sql::paths::OR; -use crate::sql::paths::SC; -use crate::sql::paths::SD; +use crate::sql::paths::RD; use crate::sql::paths::TK; use crate::sql::value::Value; +pub fn ac(ctx: &Context, _: ()) -> Result { + ctx.value("session").unwrap_or(&Value::None).pick(AC.as_ref()).ok() +} + pub fn db(ctx: &Context, _: ()) -> Result { ctx.value("session").unwrap_or(&Value::None).pick(DB.as_ref()).ok() } @@ -30,12 +34,8 @@ pub fn origin(ctx: &Context, _: ()) -> Result { ctx.value("session").unwrap_or(&Value::None).pick(OR.as_ref()).ok() } -pub fn sc(ctx: &Context, _: ()) -> Result { - ctx.value("session").unwrap_or(&Value::None).pick(SC.as_ref()).ok() -} - -pub fn sd(ctx: &Context, _: ()) -> Result { - ctx.value("session").unwrap_or(&Value::None).pick(SD.as_ref()).ok() +pub fn rd(ctx: &Context, _: ()) -> Result { + ctx.value("session").unwrap_or(&Value::None).pick(RD.as_ref()).ok() } pub fn token(ctx: &Context, _: ()) -> Result { diff --git a/core/src/iam/auth.rs b/core/src/iam/auth.rs index 80b6e795..04ab465e 100644 --- a/core/src/iam/auth.rs +++ b/core/src/iam/auth.rs @@ -1,4 +1,4 @@ -use crate::sql::statements::{DefineTokenStatement, DefineUserStatement}; +use crate::sql::statements::{DefineAccessStatement, DefineUserStatement}; use revision::revisioned; use serde::{Deserialize, Serialize}; @@ -49,9 +49,9 @@ impl Auth { matches!(self.level(), Level::Database(_, _)) } - /// Check if the current level is Scope - pub fn is_scope(&self) -> bool { - matches!(self.level(), Level::Scope(_, _, _)) + /// Check if the current level is Record + pub fn is_record(&self) -> bool { + matches!(self.level(), Level::Record(_, _, _)) } /// System Auth helpers @@ -70,8 +70,8 @@ impl Auth { 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 { - Self::new(Actor::new(rid, vec![], (ns, db, sc).into())) + pub fn for_record(rid: String, ns: &str, db: &str, ac: &str) -> Self { + 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 { - fn from(val: (&DefineTokenStatement, Level)) -> Self { +impl std::convert::From<(&DefineAccessStatement, Level)> for Auth { + fn from(val: (&DefineAccessStatement, Level)) -> Self { Self::new((val.0, val.1).into()) } } diff --git a/core/src/iam/clear.rs b/core/src/iam/clear.rs index b0acf73e..95afad5b 100644 --- a/core/src/iam/clear.rs +++ b/core/src/iam/clear.rs @@ -6,7 +6,7 @@ use std::sync::Arc; pub fn clear(session: &mut Session) -> Result<(), Error> { session.au = Arc::new(Auth::default()); session.tk = None; - session.sc = None; - session.sd = None; + session.ac = None; + session.rd = None; Ok(()) } diff --git a/core/src/iam/entities/resources/actor.rs b/core/src/iam/entities/resources/actor.rs index a7d4b59c..a2d2efd6 100644 --- a/core/src/iam/entities/resources/actor.rs +++ b/core/src/iam/entities/resources/actor.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use super::{Level, Resource, ResourceKind}; use crate::iam::Role; -use crate::sql::statements::{DefineTokenStatement, DefineUserStatement}; +use crate::sql::statements::{DefineAccessStatement, DefineUserStatement}; // // User @@ -124,8 +124,8 @@ impl std::convert::From<(&DefineUserStatement, Level)> for Actor { } } -impl std::convert::From<(&DefineTokenStatement, Level)> for Actor { - fn from(val: (&DefineTokenStatement, Level)) -> Self { +impl std::convert::From<(&DefineAccessStatement, Level)> for Actor { + fn from(val: (&DefineAccessStatement, Level)) -> Self { Self::new(val.0.name.to_string(), Vec::default(), val.1) } } diff --git a/core/src/iam/entities/resources/level.rs b/core/src/iam/entities/resources/level.rs index 30100904..bd33b239 100644 --- a/core/src/iam/entities/resources/level.rs +++ b/core/src/iam/entities/resources/level.rs @@ -17,7 +17,7 @@ pub enum Level { Root, Namespace(String), Database(String, String), - Scope(String, String, String), + Record(String, String, String), } impl std::fmt::Display for Level { @@ -27,7 +27,7 @@ impl std::fmt::Display for Level { Level::Root => write!(f, "/"), Level::Namespace(ns) => write!(f, "/ns:{ns}/"), 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::Namespace(_) => "Namespace", Level::Database(_, _) => "Database", - Level::Scope(_, _, _) => "Scope", + Level::Record(_, _, _) => "Record", } } @@ -47,7 +47,7 @@ impl Level { match self { Level::Namespace(ns) => Some(ns), Level::Database(ns, _) => Some(ns), - Level::Scope(ns, _, _) => Some(ns), + Level::Record(ns, _, _) => Some(ns), _ => None, } } @@ -55,14 +55,14 @@ impl Level { pub fn db(&self) -> Option<&str> { match self { Level::Database(_, db) => Some(db), - Level::Scope(_, db, _) => Some(db), + Level::Record(_, db, _) => Some(db), _ => None, } } - pub fn scope(&self) -> Option<&str> { + pub fn id(&self) -> Option<&str> { match self { - Level::Scope(_, _, scope) => Some(scope), + Level::Record(_, _, id) => Some(id), _ => None, } } @@ -73,7 +73,7 @@ impl Level { Level::Root => None, Level::Namespace(_) => Some(Level::Root), 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())); } - if let Some(scope) = self.scope() { - attrs.insert("scope".into(), RestrictedExpression::new_string(scope.to_owned())); + if let Some(id) = self.id() { + attrs.insert("id".into(), RestrictedExpression::new_string(id.to_owned())); } attrs @@ -139,8 +139,8 @@ impl From<(&str, &str)> for Level { } impl From<(&str, &str, &str)> for Level { - fn from((ns, db, sc): (&str, &str, &str)) -> Self { - Level::Scope(ns.to_owned(), db.to_owned(), sc.to_owned()) + fn from((ns, db, id): (&str, &str, &str)) -> Self { + 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(), (Some(ns), None, None) => (ns,).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, } } diff --git a/core/src/iam/entities/resources/resource.rs b/core/src/iam/entities/resources/resource.rs index a42b6d46..d093002e 100644 --- a/core/src/iam/entities/resources/resource.rs +++ b/core/src/iam/entities/resources/resource.rs @@ -18,7 +18,7 @@ pub enum ResourceKind { Any, Namespace, Database, - Scope, + Record, Table, Document, Option, @@ -40,7 +40,7 @@ impl std::fmt::Display for ResourceKind { ResourceKind::Any => write!(f, "Any"), ResourceKind::Namespace => write!(f, "Namespace"), ResourceKind::Database => write!(f, "Database"), - ResourceKind::Scope => write!(f, "Scope"), + ResourceKind::Record => write!(f, "Record"), ResourceKind::Table => write!(f, "Table"), ResourceKind::Document => write!(f, "Document"), ResourceKind::Option => write!(f, "Option"), @@ -74,8 +74,8 @@ impl ResourceKind { self.on_level((ns, db).into()) } - pub fn on_scope(self, ns: &str, db: &str, scope: &str) -> Resource { - self.on_level((ns, db, scope).into()) + pub fn on_record(self, ns: &str, db: &str, rid: &str) -> Resource { + self.on_level((ns, db, rid).into()) } } diff --git a/core/src/iam/entities/schema.rs b/core/src/iam/entities/schema.rs index 36db1283..533324c8 100644 --- a/core/src/iam/entities/schema.rs +++ b/core/src/iam/entities/schema.rs @@ -16,7 +16,7 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy = Lazy::new(|| { }, }, "entityTypes": { - // Represents the Root, Namespace, Database and Scope levels + // Represents the Root, Namespace, Database and Record levels "Level": { "shape": { "type": "Record", @@ -24,7 +24,7 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy = Lazy::new(|| { "type": { "type": "String", "required": true }, "ns": { "type": "String", "required": false }, "db": { "type": "String", "required": false }, - "scope": { "type": "String", "required": false }, + "rid": { "type": "String", "required": false }, "table": { "type": "String", "required": false }, "level" : { "type": "Entity", "name": "Level", "required": true }, } @@ -36,7 +36,7 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy = Lazy::new(|| { "Any": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Namespace": {"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"]}, "Document": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, "Option": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]}, @@ -65,14 +65,14 @@ pub static DEFAULT_CEDAR_SCHEMA: Lazy = Lazy::new(|| { "View": { "appliesTo": { "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": { "appliesTo": { "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" ], }, }, }, diff --git a/core/src/iam/issue.rs b/core/src/iam/issue.rs new file mode 100644 index 00000000..866cb6e0 --- /dev/null +++ b/core/src/iam/issue.rs @@ -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 { + 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) -> Result, 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) +} diff --git a/core/src/iam/mod.rs b/core/src/iam/mod.rs index 8925a0ec..b230412d 100644 --- a/core/src/iam/mod.rs +++ b/core/src/iam/mod.rs @@ -7,6 +7,7 @@ pub mod base; pub mod check; pub mod clear; pub mod entities; +pub mod issue; #[cfg(feature = "jwks")] pub mod jwks; pub mod policies; diff --git a/core/src/iam/policies/policy_set.rs b/core/src/iam/policies/policy_set.rs index 1102e9b0..dc658990 100644 --- a/core/src/iam/policies/policy_set.rs +++ b/core/src/iam/policies/policy_set.rs @@ -9,7 +9,7 @@ pub static POLICY_SET: Lazy = Lazy::new(|| { // All roles can view all resources on the same level hierarchy or below permit( principal, - action == Action::"View", + action == Action::"View", resource ) when { principal.roles.containsAny([Role::"Viewer", Role::"Editor", Role::"Owner"]) && @@ -24,7 +24,7 @@ pub static POLICY_SET: Lazy = Lazy::new(|| { ) when { principal.roles.contains(Role::"Editor") && 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 diff --git a/core/src/iam/signin.rs b/core/src/iam/signin.rs index c8b69dda..9d723174 100644 --- a/core/src/iam/signin.rs +++ b/core/src/iam/signin.rs @@ -1,15 +1,17 @@ use super::verify::{verify_creds_legacy, verify_db_creds, verify_ns_creds, verify_root_creds}; 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::err::Error; +use crate::iam::issue::{config, expiration}; use crate::iam::token::{Claims, HEADER}; use crate::iam::Auth; use crate::kvs::{Datastore, LockType::*, TransactionType::*}; +use crate::sql::AccessType; use crate::sql::Object; use crate::sql::Value; use chrono::{Duration, Utc}; -use jsonwebtoken::{encode, EncodingKey}; +use jsonwebtoken::{encode, EncodingKey, Header}; use std::sync::Arc; use uuid::Uuid; @@ -21,20 +23,20 @@ pub async fn signin( // Parse the specified variables let ns = vars.get("NS").or_else(|| vars.get("ns")); 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 - match (ns, db, sc) { - // SCOPE signin - (Some(ns), Some(db), Some(sc)) => { + match (ns, db, ac) { + // DB signin with access method + (Some(ns), Some(db), Some(ac)) => { // Process the provided values let ns = ns.to_raw_string(); let db = db.to_raw_string(); - let sc = sc.to_raw_string(); - // Attempt to signin to specified scope - super::signin::sc(kvs, session, ns, db, sc, vars).await + let ac = ac.to_raw_string(); + // Attempt to signin using specified access method + super::signin::db_access(kvs, session, ns, db, ac, vars).await } - // DB signin + // DB signin with user credentials (Some(ns), Some(db), None) => { // Get the provided user and pass let user = vars.get("user"); @@ -49,12 +51,12 @@ pub async fn signin( let user = user.to_raw_string(); let pass = pass.to_raw_string(); // 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), } } - // NS signin + // NS signin with user credentials (Some(ns), None, None) => { // Get the provided user and pass let user = vars.get("user"); @@ -68,12 +70,12 @@ pub async fn signin( let user = user.to_raw_string(); let pass = pass.to_raw_string(); // 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), } } - // KV signin + // ROOT signin with user credentials (None, None, None) => { // Get the provided user and pass let user = vars.get("user"); @@ -86,7 +88,7 @@ pub async fn signin( let user = user.to_raw_string(); let pass = pass.to_raw_string(); // 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), } @@ -95,114 +97,112 @@ pub async fn signin( } } -pub async fn sc( +pub async fn db_access( kvs: &Datastore, session: &mut Session, ns: String, db: String, - sc: String, + ac: String, vars: Object, ) -> Result, Error> { // Create a new readonly transaction let mut tx = kvs.transaction(Read, Optimistic).await?; - // Fetch the specified scope from storage - let scope = tx.get_sc(&ns, &db, &sc).await; + // Fetch the specified access method from storage + let access = tx.get_db_access(&ns, &db, &ac).await; // Ensure that the transaction is cancelled tx.cancel().await?; - // Check if the supplied Scope login exists - match scope { - Ok(sv) => { - match sv.signin { - // This scope allows signin - Some(val) => { - // Setup the query params - let vars = Some(vars.0); - // Setup the system session for finding the signin record - let mut sess = Session::editor().with_ns(&ns).with_db(&db); - sess.ip.clone_from(&session.ip); - sess.or.clone_from(&session.or); - // Compute the value with the params - match kvs.evaluate(val, &sess, vars).await { - // The signin value succeeded - Ok(val) => match val.record() { - // There is a record returned - Some(rid) => { - // Create the authentication key - let key = EncodingKey::from_secret(sv.code.as_ref()); - // Create the authentication claim - let exp = Some( - match sv.session { - Some(v) => { - // The defined session duration must be valid - match Duration::from_std(v.0) { - // The resulting session expiration must be valid - Ok(d) => match Utc::now().checked_add_signed(d) { - Some(exp) => exp, - None => { - return Err(Error::InvalidSessionExpiration) - } - }, - Err(_) => { - return Err(Error::InvalidSessionDuration) - } + // Check the provided access method exists + match access { + Ok(av) => { + // Check the access method type + // All access method types are supported except for JWT + // The JWT access method is the one that is internal to SurrealDB + // The equivalent of signing in with JWT is to authenticate it + match av.kind { + AccessType::Record(at) => { + // Check if the record access method supports issuing tokens + let iss = match at.jwt.issue { + Some(iss) => iss, + _ => return Err(Error::AccessMethodMismatch), + }; + match at.signin { + // This record access allows signin + Some(val) => { + // Setup the query params + let vars = Some(vars.0); + // Setup the system session for finding the signin record + let mut sess = Session::editor().with_ns(&ns).with_db(&db); + sess.ip.clone_from(&session.ip); + sess.or.clone_from(&session.or); + // Compute the value with the params + match kvs.evaluate(val, &sess, vars).await { + // The signin value succeeded + Ok(val) => { + match val.record() { + // There is a record returned + Some(rid) => { + // Create the authentication key + let key = config(iss.alg, iss.key)?; + // Create the authentication claim + let val = Claims { + 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(e) => match e { - Error::Thrown(_) => Err(e), - e if *INSECURE_FORWARD_SCOPE_ERRORS => Err(e), - _ => Err(Error::SigninQueryFailed), - }, + } + _ => Err(Error::AccessRecordNoSignin), } } - _ => Err(Error::ScopeNoSignin), + _ => Err(Error::AccessMethodMismatch), } } - _ => Err(Error::NoScopeFound), + _ => Err(Error::AccessNotFound), } } -pub async fn db( +pub async fn db_user( kvs: &Datastore, session: &mut Session, ns: String, @@ -258,7 +258,7 @@ pub async fn db( } } -pub async fn ns( +pub async fn ns_user( kvs: &Datastore, session: &mut Session, ns: String, @@ -312,7 +312,7 @@ pub async fn ns( } } -pub async fn root( +pub async fn root_user( kvs: &Datastore, session: &mut Session, user: String, @@ -370,14 +370,14 @@ mod tests { use std::collections::HashMap; #[tokio::test] - async fn test_signin_scope() { + async fn test_signin_record() { // Test with correct credentials { let ds = Datastore::new("memory").await.unwrap(); let sess = Session::owner().with_ns("test").with_db("test"); ds.execute( r#" - DEFINE SCOPE user SESSION 1h + DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h SIGNIN ( 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(); vars.insert("user", "user".into()); vars.insert("pass", "pass".into()); - let res = sc( + let res = db_access( &ds, &mut sess, "test".to_string(), @@ -422,10 +422,11 @@ mod tests { 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_scope()); + assert!(sess.au.is_record()); assert_eq!(sess.au.level().ns(), 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::Editor), "Auth user expected to not have Editor 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(); assert!( 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"); ds.execute( r#" - DEFINE SCOPE user SESSION 1h + DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h SIGNIN ( 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(); vars.insert("user", "user".into()); vars.insert("pass", "incorrect".into()); - let res = sc( + let res = db_access( &ds, &mut sess, "test".to_string(), @@ -492,7 +493,161 @@ mod tests { } #[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::( + &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 // @@ -507,7 +662,7 @@ mod tests { db: Some("test".to_string()), ..Default::default() }; - let res = db( + let res = db_user( &ds, &mut sess, "test".to_string(), @@ -546,7 +701,7 @@ mod tests { db: Some("test".to_string()), ..Default::default() }; - let res = db( + let res = db_user( &ds, &mut sess, "test".to_string(), @@ -578,7 +733,7 @@ mod tests { let mut sess = Session { ..Default::default() }; - let res = db( + let res = db_user( &ds, &mut sess, "test".to_string(), @@ -593,7 +748,7 @@ mod tests { } #[tokio::test] - async fn test_signin_ns() { + async fn test_signin_ns_user() { // // Test without roles defined // @@ -608,7 +763,7 @@ mod tests { ..Default::default() }; 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; assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res); @@ -638,7 +793,7 @@ mod tests { ..Default::default() }; 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; assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res); @@ -661,16 +816,21 @@ mod tests { let mut sess = Session { ..Default::default() }; - let res = - ns(&ds, &mut sess, "test".to_string(), "user".to_string(), "invalid".to_string()) - .await; + let res = ns_user( + &ds, + &mut sess, + "test".to_string(), + "user".to_string(), + "invalid".to_string(), + ) + .await; assert!(res.is_err(), "Unexpected successful signin: {:?}", res); } } #[tokio::test] - async fn test_signin_root() { + async fn test_signin_root_user() { // // Test without roles defined // @@ -683,7 +843,7 @@ mod tests { let mut sess = Session { ..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_eq!(sess.au.id(), "user"); @@ -708,7 +868,7 @@ mod tests { let mut sess = Session { ..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_eq!(sess.au.id(), "user"); @@ -728,7 +888,7 @@ mod tests { let mut sess = Session { ..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); } diff --git a/core/src/iam/signup.rs b/core/src/iam/signup.rs index c9ad7c6d..474b6e83 100644 --- a/core/src/iam/signup.rs +++ b/core/src/iam/signup.rs @@ -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::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::{Actor, Level}; use crate::kvs::{Datastore, LockType::*, TransactionType::*}; +use crate::sql::AccessType; use crate::sql::Object; use crate::sql::Value; -use chrono::{Duration, Utc}; -use jsonwebtoken::{encode, EncodingKey}; +use chrono::Utc; +use jsonwebtoken::{encode, Header}; use std::sync::Arc; use uuid::Uuid; @@ -20,125 +22,122 @@ pub async fn signup( // Parse the specified variables let ns = vars.get("NS").or_else(|| vars.get("ns")); 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 - match (ns, db, sc) { - (Some(ns), Some(db), Some(sc)) => { + match (ns, db, ac) { + (Some(ns), Some(db), Some(ac)) => { // Process the provided values let ns = ns.to_raw_string(); let db = db.to_raw_string(); - let sc = sc.to_raw_string(); - // Attempt to signup to specified scope - super::signup::sc(kvs, session, ns, db, sc, vars).await + let ac = ac.to_raw_string(); + // Attempt to signup using specified access method + // Currently, signup is only supported at the database level + super::signup::db_access(kvs, session, ns, db, ac, vars).await } _ => Err(Error::InvalidSignup), } } -pub async fn sc( +pub async fn db_access( kvs: &Datastore, session: &mut Session, ns: String, db: String, - sc: String, + ac: String, vars: Object, ) -> Result, Error> { // Create a new readonly transaction let mut tx = kvs.transaction(Read, Optimistic).await?; - // Fetch the specified scope from storage - let scope = tx.get_sc(&ns, &db, &sc).await; + // Fetch the specified access method from storage + let access = tx.get_db_access(&ns, &db, &ac).await; // Ensure that the transaction is cancelled tx.cancel().await?; - // Check if the supplied Scope login exists - match scope { - Ok(sv) => { - match sv.signup { - // This scope allows signup - Some(val) => { - // Setup the query params - let vars = Some(vars.0); - // Setup the system session for creating the signup record - let mut sess = Session::editor().with_ns(&ns).with_db(&db); - sess.ip.clone_from(&session.ip); - sess.or.clone_from(&session.or); - // Compute the value with the params - match kvs.evaluate(val, &sess, vars).await { - // The signin value succeeded - Ok(val) => match val.record() { - // There is a record returned - Some(rid) => { - // Create the authentication key - let key = EncodingKey::from_secret(sv.code.as_ref()); - // Create the authentication claim - let exp = Some( - match sv.session { - Some(v) => { - // The defined session duration must be valid - match Duration::from_std(v.0) { - // The resulting session expiration must be valid - Ok(d) => match Utc::now().checked_add_signed(d) { - Some(exp) => exp, - None => { - return Err(Error::InvalidSessionExpiration) - } - }, - Err(_) => { - return Err(Error::InvalidSessionDuration) - } + // Check the provided access method exists + match access { + Ok(av) => { + // Check the access method type + // Currently, only the record access method supports signup + match av.kind { + AccessType::Record(at) => { + // Check if the record access method supports issuing tokens + let iss = match at.jwt.issue { + Some(iss) => iss, + _ => return Err(Error::AccessMethodMismatch), + }; + match at.signup { + // This record access allows signup + Some(val) => { + // Setup the query params + let vars = Some(vars.0); + // Setup the system session for finding the signup record + let mut sess = Session::editor().with_ns(&ns).with_db(&db); + sess.ip.clone_from(&session.ip); + sess.or.clone_from(&session.or); + // Compute the value with the params + match kvs.evaluate(val, &sess, vars).await { + // The signin value succeeded + Ok(val) => { + match val.record() { + // There is a record returned + Some(rid) => { + // Create the authentication key + let key = config(iss.alg, iss.key)?; + // Create the authentication claim + let val = Claims { + 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 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(e) => match e { - Error::Thrown(_) => Err(e), - e if *INSECURE_FORWARD_SCOPE_ERRORS => Err(e), - _ => Err(Error::SignupQueryFailed), - }, + } + _ => Err(Error::AccessRecordNoSignup), } } - _ => Err(Error::ScopeNoSignup), + _ => Err(Error::AccessMethodMismatch), } } - _ => Err(Error::NoScopeFound), + _ => Err(Error::AccessNotFound), } } @@ -146,17 +145,18 @@ pub async fn sc( mod tests { use super::*; use crate::iam::Role; + use chrono::Duration; use std::collections::HashMap; #[tokio::test] - async fn test_scope_signup() { + async fn test_record_signup() { // Test with valid parameters { let ds = Datastore::new("memory").await.unwrap(); let sess = Session::owner().with_ns("test").with_db("test"); ds.execute( r#" - DEFINE SCOPE user SESSION 1h + DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h SIGNIN ( 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(); vars.insert("user", "user".into()); vars.insert("pass", "pass".into()); - let res = sc( + let res = db_access( &ds, &mut sess, "test".to_string(), @@ -196,10 +196,10 @@ mod tests { 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_scope()); + assert!(sess.au.is_record()); assert_eq!(sess.au.level().ns(), 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::Editor), "Auth user expected to not have Editor 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(); assert!( 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"); ds.execute( r#" - DEFINE SCOPE user SESSION 1h + DEFINE ACCESS user ON DATABASE TYPE RECORD DURATION 1h SIGNIN ( 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(); // Password is missing vars.insert("user", "user".into()); - let res = sc( + let res = db_access( &ds, &mut sess, "test".to_string(), @@ -259,4 +259,158 @@ mod tests { 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::( + &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") + } + } + } } diff --git a/core/src/iam/token.rs b/core/src/iam/token.rs index ff34dc17..f93b2ccf 100644 --- a/core/src/iam/token.rs +++ b/core/src/iam/token.rs @@ -35,20 +35,13 @@ pub struct Claims { #[serde(alias = "https://surrealdb.com/database")] #[serde(skip_serializing_if = "Option::is_none")] pub db: Option, - #[serde(alias = "sc")] - #[serde(alias = "SC")] - #[serde(rename = "SC")] - #[serde(alias = "https://surrealdb.com/sc")] - #[serde(alias = "https://surrealdb.com/scope")] + #[serde(alias = "ac")] + #[serde(alias = "AC")] + #[serde(rename = "AC")] + #[serde(alias = "https://surrealdb.com/ac")] + #[serde(alias = "https://surrealdb.com/access")] #[serde(skip_serializing_if = "Option::is_none")] - pub sc: Option, - #[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, + pub ac: Option, #[serde(alias = "id")] #[serde(alias = "ID")] #[serde(rename = "ID")] @@ -101,13 +94,9 @@ impl From for Value { if let Some(db) = v.db { out.insert("DB".to_string(), db.into()); } - // Add SC field if set - if let Some(sc) = v.sc { - out.insert("SC".to_string(), sc.into()); - } - // Add TK field if set - if let Some(tk) = v.tk { - out.insert("TK".to_string(), tk.into()); + // Add AC field if set + if let Some(ac) = v.ac { + out.insert("AC".to_string(), ac.into()); } // Add ID field if set if let Some(id) = v.id { diff --git a/core/src/iam/verify.rs b/core/src/iam/verify.rs index 5d1c1eb0..ce7dec25 100644 --- a/core/src/iam/verify.rs +++ b/core/src/iam/verify.rs @@ -4,94 +4,70 @@ use crate::err::Error; use crate::iam::jwks; use crate::iam::{token::Claims, Actor, Auth, Level, Role}; use crate::kvs::{Datastore, LockType::*, TransactionType::*}; +use crate::sql::access_type::{AccessType, JwtAccessVerify}; use crate::sql::{statements::DefineUserStatement, Algorithm, Value}; use crate::syn; use argon2::{Argon2, PasswordHash, PasswordVerifier}; use chrono::Utc; -use jsonwebtoken::{decode, DecodingKey, Header, Validation}; +use jsonwebtoken::{decode, DecodingKey, Validation}; use once_cell::sync::Lazy; use std::str::{self, FromStr}; use std::sync::Arc; -async fn config( - _kvs: &Datastore, - 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 { +fn config(alg: Algorithm, key: String) -> Result<(DecodingKey, Validation), Error> { + match alg { Algorithm::Hs256 => Ok(( - DecodingKey::from_secret(code.as_ref()), + DecodingKey::from_secret(key.as_ref()), Validation::new(jsonwebtoken::Algorithm::HS256), )), Algorithm::Hs384 => Ok(( - DecodingKey::from_secret(code.as_ref()), + DecodingKey::from_secret(key.as_ref()), Validation::new(jsonwebtoken::Algorithm::HS384), )), Algorithm::Hs512 => Ok(( - DecodingKey::from_secret(code.as_ref()), + DecodingKey::from_secret(key.as_ref()), Validation::new(jsonwebtoken::Algorithm::HS512), )), Algorithm::EdDSA => Ok(( - DecodingKey::from_ed_pem(code.as_ref())?, + DecodingKey::from_ed_pem(key.as_ref())?, Validation::new(jsonwebtoken::Algorithm::EdDSA), )), Algorithm::Es256 => Ok(( - DecodingKey::from_ec_pem(code.as_ref())?, + DecodingKey::from_ec_pem(key.as_ref())?, Validation::new(jsonwebtoken::Algorithm::ES256), )), Algorithm::Es384 => Ok(( - DecodingKey::from_ec_pem(code.as_ref())?, + DecodingKey::from_ec_pem(key.as_ref())?, Validation::new(jsonwebtoken::Algorithm::ES384), )), Algorithm::Es512 => Ok(( - DecodingKey::from_ec_pem(code.as_ref())?, + DecodingKey::from_ec_pem(key.as_ref())?, Validation::new(jsonwebtoken::Algorithm::ES384), )), Algorithm::Ps256 => Ok(( - DecodingKey::from_rsa_pem(code.as_ref())?, + DecodingKey::from_rsa_pem(key.as_ref())?, Validation::new(jsonwebtoken::Algorithm::PS256), )), Algorithm::Ps384 => Ok(( - DecodingKey::from_rsa_pem(code.as_ref())?, + DecodingKey::from_rsa_pem(key.as_ref())?, Validation::new(jsonwebtoken::Algorithm::PS384), )), Algorithm::Ps512 => Ok(( - DecodingKey::from_rsa_pem(code.as_ref())?, + DecodingKey::from_rsa_pem(key.as_ref())?, Validation::new(jsonwebtoken::Algorithm::PS512), )), Algorithm::Rs256 => Ok(( - DecodingKey::from_rsa_pem(code.as_ref())?, + DecodingKey::from_rsa_pem(key.as_ref())?, Validation::new(jsonwebtoken::Algorithm::RS256), )), Algorithm::Rs384 => Ok(( - DecodingKey::from_rsa_pem(code.as_ref())?, + DecodingKey::from_rsa_pem(key.as_ref())?, Validation::new(jsonwebtoken::Algorithm::RS384), )), Algorithm::Rs512 => Ok(( - DecodingKey::from_rsa_pem(code.as_ref())?, + DecodingKey::from_rsa_pem(key.as_ref())?, 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 match token_data.claims { - // Check if this is scope token authentication + // Check if this is record access Claims { ns: Some(ns), db: Some(db), - sc: Some(sc), - 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::(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), + ac: Some(ac), id: Some(id), .. } => { // Log the decoded authentication claims - trace!("Authenticating to scope `{}`", sc); + trace!("Authenticating with record access method `{}`", ac); // Create a new readonly transaction let mut tx = kvs.transaction(Read, Optimistic).await?; // Parse the record id let id = syn::thing(&id)?; - // Get the scope - let de = tx.get_sc(&ns, &db, &sc).await?; - let cf = config_alg(Algorithm::Hs512, de.code)?; + // Get the database access method + let de = tx.get_db_access(&ns, &db, &ac).await?; + // 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 decode::(token, &cf.0, &cf.1)?; // Log the success - debug!("Authenticated to scope `{}`", sc); + debug!("Authenticated with record access method `{}`", ac); // Set the session session.tk = Some(value); session.ns = Some(ns.to_owned()); session.db = Some(db.to_owned()); - session.sc = Some(sc.to_owned()); - session.sd = Some(Value::from(id.to_owned())); + session.ac = Some(ac.to_owned()); + session.rd = Some(Value::from(id.to_owned())); session.exp = token_data.claims.exp; session.au = Arc::new(Auth::new(Actor::new( id.to_string(), Default::default(), - Level::Scope(ns, db, sc), + Level::Record(ns, db, id.to_string()), ))); Ok(()) } - // Check if this is database token authentication + // Check if this is database access Claims { ns: Some(ns), db: Some(db), - tk: Some(tk), + ac: Some(ac), .. } => { // 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 let mut tx = kvs.transaction(Read, Optimistic).await?; - // Get the database token - let de = tx.get_db_token(&ns, &db, &tk).await?; - // Obtain the configuration with which to verify the token - let cf = config(kvs, de.kind, de.code, token_data.header).await?; + // Get the database access method + let de = tx.get_db_access(&ns, &db, &ac).await?; + // Obtain the configuration to verify the token based on the access method + 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 decode::(token, &cf.0, &cf.1)?; // Parse the roles @@ -320,11 +287,12 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul .collect::, _>>()?, }; // Log the success - debug!("Authenticated to database `{}` with token `{}`", db, tk); + debug!("Authenticated to database `{}` with access method `{}`", db, ac); // Set the session session.tk = Some(value); session.ns = Some(ns.to_owned()); session.db = Some(db.to_owned()); + session.ac = Some(ac.to_owned()); session.exp = token_data.claims.exp; session.au = Arc::new(Auth::new(Actor::new( de.name.to_string(), @@ -333,7 +301,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul ))); Ok(()) } - // Check if this is database authentication + // Check if this is database authentication with user credentials Claims { ns: Some(ns), 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}"); Error::InvalidAuth })?; - let cf = config_alg(Algorithm::Hs512, de.code)?; + let cf = config(Algorithm::Hs512, de.code)?; // Verify the token decode::(token, &cf.0, &cf.1)?; // Log the success @@ -366,20 +334,35 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul ))); Ok(()) } - // Check if this is namespace token authentication + // Check if this is namespace access Claims { ns: Some(ns), - tk: Some(tk), + ac: Some(ac), .. } => { // 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 let mut tx = kvs.transaction(Read, Optimistic).await?; - // Get the namespace token - let de = tx.get_ns_token(&ns, &tk).await?; - // Obtain the configuration with which to verify the token - let cf = config(kvs, de.kind, de.code, token_data.header).await?; + // Get the namespace access method + let de = tx.get_ns_access(&ns, &ac).await?; + // Obtain the configuration to verify the token based on the access method + 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 decode::(token, &cf.0, &cf.1)?; // Parse the roles @@ -395,16 +378,17 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul .collect::, _>>()?, }; // Log the success - trace!("Authenticated to namespace `{}` with token `{}`", ns, tk); + trace!("Authenticated to namespace `{}` with access method `{}`", ns, ac); // Set the session session.tk = Some(value); session.ns = Some(ns.to_owned()); + session.ac = Some(ac.to_owned()); session.exp = token_data.claims.exp; session.au = Arc::new(Auth::new(Actor::new(de.name.to_string(), roles, Level::Namespace(ns)))); Ok(()) } - // Check if this is namespace authentication + // Check if this is namespace authentication with user credentials Claims { ns: Some(ns), 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}"); Error::InvalidAuth })?; - let cf = config_alg(Algorithm::Hs512, de.code)?; + let cf = config(Algorithm::Hs512, de.code)?; // Verify the token decode::(token, &cf.0, &cf.1)?; // Log the success @@ -435,7 +419,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul ))); Ok(()) } - // Check if this is root level authentication + // Check if this is root authentication with user credentials Claims { 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}"); Error::InvalidAuth })?; - let cf = config_alg(Algorithm::Hs512, de.code)?; + let cf = config(Algorithm::Hs512, de.code)?; // Verify the token decode::(token, &cf.0, &cf.1)?; // Log the success @@ -814,7 +798,7 @@ mod tests { iat: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()), exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - tk: Some("token".to_string()), + ac: Some("token".to_string()), ns: Some("test".to_string()), ..Claims::default() }; @@ -822,7 +806,7 @@ mod tests { let ds = Datastore::new("memory").await.unwrap(); let sess = Session::owner().with_ns("test").with_db("test"); 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, None, ) @@ -921,7 +905,7 @@ mod tests { iat: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()), exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - tk: Some("token".to_string()), + ac: Some("token".to_string()), ns: Some("test".to_string()), db: Some("test".to_string()), ..Claims::default() @@ -930,7 +914,8 @@ mod tests { let ds = Datastore::new("memory").await.unwrap(); let sess = Session::owner().with_ns("test").with_db("test"); 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, None, ) @@ -1023,7 +1008,7 @@ mod tests { } #[tokio::test] - async fn test_token_scope() { + async fn test_token_db_record() { let secret = "jwt_secret"; let key = EncodingKey::from_secret(secret.as_ref()); let claims = Claims { @@ -1031,17 +1016,25 @@ mod tests { iat: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()), exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - tk: Some("token".to_string()), ns: 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() }; let ds = Datastore::new("memory").await.unwrap(); let sess = Session::owner().with_ns("test").with_db("test"); 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, None, ) @@ -1050,7 +1043,7 @@ mod tests { // // Test without roles defined - // Roles should be ignored in scope authentication + // Roles should be ignored in record access // { // Prepare the claims object @@ -1065,9 +1058,9 @@ mod tests { assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.sc, Some("test".to_string())); - assert_eq!(sess.au.id(), "token"); - assert!(sess.au.is_scope()); + assert_eq!(sess.ac, Some("token".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!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); @@ -1078,7 +1071,7 @@ mod tests { // // Test with roles defined - // Roles should be ignored in scope authentication + // Roles should be ignored in record access // { // Prepare the claims object @@ -1093,9 +1086,9 @@ mod tests { assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.sc, Some("test".to_string())); - assert_eq!(sess.au.id(), "token"); - assert!(sess.au.is_scope()); + assert_eq!(sess.ac, Some("token".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!(!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 let mut claims = claims.clone(); - claims.sc = Some("invalid".to_string()); + claims.ac = Some("invalid".to_string()); // Create the token let enc = encode(&HEADER, &claims, &key).unwrap(); // Signin with the token @@ -1156,7 +1149,7 @@ mod tests { // Test with generic user identifier // { - let resource_id = "user:`2k9qnabxuxh8k4d5gfto`".to_string(); + let resource_id = "user:⟨2k9qnabxuxh8k4d5gfto⟩".to_string(); // Prepare the claims object let mut claims = claims.clone(); claims.id = Some(resource_id.clone()); @@ -1169,11 +1162,11 @@ mod tests { assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.sc, Some("test".to_string())); - assert_eq!(sess.au.id(), "token"); - assert!(sess.au.is_scope()); + assert_eq!(sess.ac, Some("token".to_string())); + assert_eq!(sess.au.id(), resource_id); + assert!(sess.au.is_record()); 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_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.sc, Some("test".to_string())); - assert_eq!(sess.au.id(), "token"); - assert!(sess.au.is_scope()); + assert_eq!(sess.ac, Some("token".to_string())); + assert_eq!(sess.au.id(), resource_id); + assert!(sess.au.is_record()); 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_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.sc, Some("test".to_string())); - assert_eq!(sess.au.id(), "token"); - assert!(sess.au.is_scope()); + assert_eq!(sess.ac, Some("token".to_string())); + assert_eq!(sess.au.id(), resource_id); + assert!(sess.au.is_record()); 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_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.sc, Some("test".to_string())); - assert_eq!(sess.au.id(), "token"); - assert!(sess.au.is_scope()); + assert_eq!(sess.ac, Some("token".to_string())); + assert_eq!(sess.au.id(), resource_id); + assert!(sess.au.is_record()); 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_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.sc, Some("test".to_string())); - assert_eq!(sess.au.id(), "token"); - assert!(sess.au.is_scope()); + assert_eq!(sess.ac, Some("token".to_string())); + assert_eq!(sess.au.id(), resource_id); + assert!(sess.au.is_record()); 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] - async fn test_token_scope_custom_claims() { + async fn test_token_db_record_custom_claims() { use std::collections::HashMap; let secret = "jwt_secret"; @@ -1295,7 +1288,15 @@ mod tests { let ds = Datastore::new("memory").await.unwrap(); let sess = Session::owner().with_ns("test").with_db("test"); 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, None, ) @@ -1315,10 +1316,10 @@ mod tests { "iat": {now}, "nbf": {now}, "exp": {later}, - "tk": "token", "ns": "test", "db": "test", - "sc": "test", + "ac": "token", + "id": "user:test", "string_claim": "test", "bool_claim": true, @@ -1351,9 +1352,9 @@ mod tests { assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.sc, Some("test".to_string())); - assert_eq!(sess.au.id(), "token"); - assert!(sess.au.is_scope()); + assert_eq!(sess.ac, Some("token".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!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); @@ -1387,7 +1388,7 @@ mod tests { #[cfg(feature = "jwks")] #[tokio::test] - async fn test_token_scope_jwks() { + async fn test_token_db_record_jwks() { use crate::dbs::capabilities::{Capabilities, NetTarget, Targets}; use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine}; use jsonwebtoken::jwk::{Jwk, JwkSet}; @@ -1448,8 +1449,15 @@ mod tests { let sess = Session::owner().with_ns("test").with_db("test"); ds.execute( - format!("DEFINE TOKEN token ON SCOPE test TYPE JWKS VALUE '{server_url}/{jwks_path}';") - .as_str(), + format!( + r#" + DEFINE ACCESS token ON DATABASE TYPE RECORD + WITH JWT URL '{server_url}/{jwks_path}'; + + CREATE user:test; + "# + ) + .as_str(), &sess, None, ) @@ -1470,16 +1478,16 @@ mod tests { iat: Some(Utc::now().timestamp()), nbf: Some(Utc::now().timestamp()), exp: Some((Utc::now() + Duration::hours(1)).timestamp()), - tk: Some("token".to_string()), ns: 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() }; // // Test without roles defined - // Roles should be ignored in scope authentication + // Roles should be ignored in record access // { // Prepare the claims object @@ -1494,9 +1502,9 @@ mod tests { assert!(res.is_ok(), "Failed to signin with token: {:?}", res); assert_eq!(sess.ns, Some("test".to_string())); assert_eq!(sess.db, Some("test".to_string())); - assert_eq!(sess.sc, Some("test".to_string())); - assert_eq!(sess.au.id(), "token"); - assert!(sess.au.is_scope()); + assert_eq!(sess.ac, Some("token".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!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role"); diff --git a/core/src/key/database/tk.rs b/core/src/key/database/ac.rs similarity index 57% rename from core/src/key/database/tk.rs rename to core/src/key/database/ac.rs index 0af2c79e..519a6e38 100644 --- a/core/src/key/database/tk.rs +++ b/core/src/key/database/ac.rs @@ -1,12 +1,12 @@ +/// Stores a DEFINE ACCESS ON DATABASE config definition use crate::key::error::KeyCategory; use crate::key::key_req::KeyRequirements; -/// Stores a DEFINE TOKEN ON DATABASE config definition use derive::Key; use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)] #[non_exhaustive] -pub struct Tk<'a> { +pub struct Ac<'a> { __: u8, _a: u8, pub ns: &'a str, @@ -15,33 +15,33 @@ pub struct Tk<'a> { _c: u8, _d: 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> { - Tk::new(ns, db, tk) +pub fn new<'a>(ns: &'a str, db: &'a str, ac: &'a str) -> Ac<'a> { + Ac::new(ns, db, ac) } pub fn prefix(ns: &str, db: &str) -> Vec { 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 } pub fn suffix(ns: &str, db: &str) -> Vec { 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 } -impl KeyRequirements for Tk<'_> { +impl KeyRequirements for Ac<'_> { fn key_category(&self) -> KeyCategory { - KeyCategory::DatabaseToken + KeyCategory::DatabaseAccess } } -impl<'a> Tk<'a> { - pub fn new(ns: &'a str, db: &'a str, tk: &'a str) -> Self { +impl<'a> Ac<'a> { + pub fn new(ns: &'a str, db: &'a str, ac: &'a str) -> Self { Self { __: b'/', _a: b'*', @@ -49,9 +49,9 @@ impl<'a> Tk<'a> { _b: b'*', db, _c: b'!', - _d: b't', - _e: b'k', - tk, + _d: b'a', + _e: b'c', + ac, } } } @@ -62,27 +62,27 @@ mod tests { fn key() { use super::*; #[rustfmt::skip] - let val = Tk::new( + let val = Ac::new( "testns", "testdb", - "testtk", + "testac", ); - let enc = Tk::encode(&val).unwrap(); - assert_eq!(enc, b"/*testns\x00*testdb\x00!tktesttk\x00"); + let enc = Ac::encode(&val).unwrap(); + 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); } #[test] fn test_prefix() { 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] fn test_suffix() { 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"); } } diff --git a/core/src/key/database/mod.rs b/core/src/key/database/mod.rs index ec74c794..33923432 100644 --- a/core/src/key/database/mod.rs +++ b/core/src/key/database/mod.rs @@ -1,12 +1,11 @@ +pub mod ac; pub mod all; pub mod az; pub mod fc; pub mod ml; pub mod pa; -pub mod sc; pub mod tb; pub mod ti; -pub mod tk; pub mod ts; pub mod us; pub mod vs; diff --git a/core/src/key/database/sc.rs b/core/src/key/database/sc.rs deleted file mode 100644 index 76d5696e..00000000 --- a/core/src/key/database/sc.rs +++ /dev/null @@ -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 { - 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 { - 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); - } -} diff --git a/core/src/key/error.rs b/core/src/key/error.rs index af1d3d9f..bd70a856 100644 --- a/core/src/key/error.rs +++ b/core/src/key/error.rs @@ -8,6 +8,8 @@ pub enum KeyCategory { Unknown, /// crate::key::root::all / Root, + /// crate::key::root::ac /!ac{ac} + Access, /// crate::key::root::hb /!hb{ts}/{nd} Heartbeat, /// crate::key::root::nd /!nd{nd} @@ -32,13 +34,15 @@ pub enum KeyCategory { DatabaseIdentifier, /// crate::key::namespace::lg /*{ns}!lg{lg} DatabaseLogAlias, - /// crate::key::namespace::tk /*{ns}!tk{tk} - NamespaceToken, + /// crate::key::namespace::ac /*{ns}!ac{ac} + NamespaceAccess, /// crate::key::namespace::us /*{ns}!us{us} NamespaceUser, /// /// crate::key::database::all /*{ns}*{db} DatabaseRoot, + /// crate::key::database::ac /*{ns}*{db}!ac{ac} + DatabaseAccess, /// crate::key::database::az /*{ns}*{db}!az{az} DatabaseAnalyzer, /// crate::key::database::fc /*{ns}*{db}!fn{fc} @@ -49,14 +53,10 @@ pub enum KeyCategory { DatabaseModel, /// crate::key::database::pa /*{ns}*{db}!pa{pa} DatabaseParameter, - /// crate::key::database::sc /*{ns}*{db}!sc{sc} - DatabaseScope, /// crate::key::database::tb /*{ns}*{db}!tb{tb} DatabaseTable, /// crate::key::database::ti /+{ns id}*{db id}!ti DatabaseTableIdentifier, - /// crate::key::database::tk /*{ns}*{db}!tk{tk} - DatabaseToken, /// crate::key::database::ts /*{ns}*{db}!ts{ts} DatabaseTimestamp, /// crate::key::database::us /*{ns}*{db}!us{us} @@ -64,11 +64,6 @@ pub enum KeyCategory { /// crate::key::database::vs /*{ns}*{db}!vs 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} TableRoot, /// crate::key::table::ev /*{ns}*{db}*{tb}!ev{ev} @@ -124,6 +119,7 @@ impl Display for KeyCategory { let name = match self { KeyCategory::Unknown => "Unknown", KeyCategory::Root => "Root", + KeyCategory::Access => "Access", KeyCategory::Heartbeat => "Heartbeat", KeyCategory::Node => "Node", KeyCategory::NamespaceIdentifier => "NamespaceIdentifier", @@ -135,23 +131,20 @@ impl Display for KeyCategory { KeyCategory::DatabaseAlias => "DatabaseAlias", KeyCategory::DatabaseIdentifier => "DatabaseIdentifier", KeyCategory::DatabaseLogAlias => "DatabaseLogAlias", - KeyCategory::NamespaceToken => "NamespaceToken", + KeyCategory::NamespaceAccess => "NamespaceAccess", KeyCategory::NamespaceUser => "NamespaceUser", KeyCategory::DatabaseRoot => "DatabaseRoot", + KeyCategory::DatabaseAccess => "DatabaseAccess", KeyCategory::DatabaseAnalyzer => "DatabaseAnalyzer", KeyCategory::DatabaseFunction => "DatabaseFunction", KeyCategory::DatabaseLog => "DatabaseLog", KeyCategory::DatabaseModel => "DatabaseModel", KeyCategory::DatabaseParameter => "DatabaseParameter", - KeyCategory::DatabaseScope => "DatabaseScope", KeyCategory::DatabaseTable => "DatabaseTable", KeyCategory::DatabaseTableIdentifier => "DatabaseTableIdentifier", - KeyCategory::DatabaseToken => "DatabaseToken", KeyCategory::DatabaseTimestamp => "DatabaseTimestamp", KeyCategory::DatabaseUser => "DatabaseUser", KeyCategory::DatabaseVersionstamp => "DatabaseVersionstamp", - KeyCategory::ScopeRoot => "ScopeRoot", - KeyCategory::ScopeToken => "ScopeToken", KeyCategory::TableRoot => "TableRoot", KeyCategory::TableEvent => "TableEvent", KeyCategory::TableField => "TableField", diff --git a/core/src/key/mod.rs b/core/src/key/mod.rs index cd692d28..44285d82 100644 --- a/core/src/key/mod.rs +++ b/core/src/key/mod.rs @@ -11,28 +11,24 @@ /// crate::key::node::lq /${nd}!lq{lq}{ns}{db} /// /// crate::key::namespace::all /*{ns} +/// crate::key::namespace::ac /*{ns}!ac{ac} /// crate::key::namespace::db /*{ns}!db{db} /// crate::key::namespace::di /+{ns id}!di /// crate::key::namespace::lg /*{ns}!lg{lg} -/// crate::key::namespace::tk /*{ns}!tk{tk} /// crate::key::namespace::us /*{ns}!us{us} /// /// 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::fc /*{ns}*{db}!fn{fc} /// crate::key::database::lg /*{ns}*{db}!lg{lg} /// 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::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::us /*{ns}*{db}!us{us} /// 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::ev /*{ns}*{db}*{tb}!ev{ev} /// crate::key::table::fd /*{ns}*{db}*{tb}!fd{fd} @@ -70,6 +66,5 @@ pub(crate) mod key_req; pub mod namespace; pub mod node; pub mod root; -pub mod scope; pub mod table; pub mod thing; diff --git a/core/src/key/namespace/tk.rs b/core/src/key/namespace/ac.rs similarity index 55% rename from core/src/key/namespace/tk.rs rename to core/src/key/namespace/ac.rs index ca324b68..0bb8f859 100644 --- a/core/src/key/namespace/tk.rs +++ b/core/src/key/namespace/ac.rs @@ -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::key_req::KeyRequirements; use derive::Key; @@ -6,48 +6,48 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)] #[non_exhaustive] -pub struct Tk<'a> { +pub struct Ac<'a> { __: u8, _a: u8, pub ns: &'a str, _b: u8, _c: u8, _d: u8, - pub tk: &'a str, + pub ac: &'a str, } -pub fn new<'a>(ns: &'a str, tk: &'a str) -> Tk<'a> { - Tk::new(ns, tk) +pub fn new<'a>(ns: &'a str, ac: &'a str) -> Ac<'a> { + Ac::new(ns, ac) } pub fn prefix(ns: &str) -> Vec { 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 } pub fn suffix(ns: &str) -> Vec { 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 } -impl KeyRequirements for Tk<'_> { +impl KeyRequirements for Ac<'_> { fn key_category(&self) -> KeyCategory { - KeyCategory::NamespaceToken + KeyCategory::NamespaceAccess } } -impl<'a> Tk<'a> { - pub fn new(ns: &'a str, tk: &'a str) -> Self { +impl<'a> Ac<'a> { + pub fn new(ns: &'a str, ac: &'a str) -> Self { Self { __: b'/', _a: b'*', ns, _b: b'!', - _c: b't', - _d: b'k', - tk, + _c: b'a', + _d: b'c', + ac, } } } @@ -58,13 +58,13 @@ mod tests { fn key() { use super::*; #[rustfmt::skip] - let val = Tk::new( + let val = Ac::new( "testns", - "testtk", + "testac", ); - let enc = Tk::encode(&val).unwrap(); - assert_eq!(enc, b"/*testns\0!tktesttk\0"); - let dec = Tk::decode(&enc).unwrap(); + let enc = Ac::encode(&val).unwrap(); + assert_eq!(enc, b"/*testns\0!actestac\0"); + let dec = Ac::decode(&enc).unwrap(); assert_eq!(val, dec); } } diff --git a/core/src/key/namespace/mod.rs b/core/src/key/namespace/mod.rs index a3e6d74a..2feaaeb0 100644 --- a/core/src/key/namespace/mod.rs +++ b/core/src/key/namespace/mod.rs @@ -1,5 +1,5 @@ +pub mod ac; pub mod all; pub mod db; pub mod di; -pub mod tk; pub mod us; diff --git a/core/src/key/root/ac.rs b/core/src/key/root/ac.rs new file mode 100644 index 00000000..cf965fba --- /dev/null +++ b/core/src/key/root/ac.rs @@ -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 { + let mut k = super::all::new().encode().unwrap(); + k.extend_from_slice(&[b'!', b'a', b'c', 0x00]); + k +} + +pub fn suffix() -> Vec { + let mut k = super::all::new().encode().unwrap(); + 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"); + } +} diff --git a/core/src/key/root/mod.rs b/core/src/key/root/mod.rs index 5109a691..4d011716 100644 --- a/core/src/key/root/mod.rs +++ b/core/src/key/root/mod.rs @@ -1,3 +1,4 @@ +pub mod ac; pub mod all; pub mod hb; pub mod nd; diff --git a/core/src/key/scope/all.rs b/core/src/key/scope/all.rs deleted file mode 100644 index 5357b294..00000000 --- a/core/src/key/scope/all.rs +++ /dev/null @@ -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); - } -} diff --git a/core/src/key/scope/mod.rs b/core/src/key/scope/mod.rs deleted file mode 100644 index 24da2790..00000000 --- a/core/src/key/scope/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod all; -pub mod tk; - -const CHAR: u8 = 0xb1; // ± diff --git a/core/src/key/scope/tk.rs b/core/src/key/scope/tk.rs deleted file mode 100644 index 8f1e7fae..00000000 --- a/core/src/key/scope/tk.rs +++ /dev/null @@ -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 { - 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 { - 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); - } -} diff --git a/core/src/kvs/cache.rs b/core/src/kvs/cache.rs index 4c61acea..8fea22fb 100644 --- a/core/src/kvs/cache.rs +++ b/core/src/kvs/cache.rs @@ -1,5 +1,6 @@ use crate::idg::u32::U32; use crate::kvs::kv::Key; +use crate::sql::statements::DefineAccessStatement; use crate::sql::statements::DefineAnalyzerStatement; use crate::sql::statements::DefineDatabaseStatement; use crate::sql::statements::DefineEventStatement; @@ -9,9 +10,7 @@ use crate::sql::statements::DefineIndexStatement; use crate::sql::statements::DefineModelStatement; use crate::sql::statements::DefineNamespaceStatement; use crate::sql::statements::DefineParamStatement; -use crate::sql::statements::DefineScopeStatement; use crate::sql::statements::DefineTableStatement; -use crate::sql::statements::DefineTokenStatement; use crate::sql::statements::DefineUserStatement; use crate::sql::statements::LiveStatement; use std::collections::HashMap; @@ -31,7 +30,7 @@ pub enum Entry { // Multi definitions Azs(Arc<[DefineAnalyzerStatement]>), Dbs(Arc<[DefineDatabaseStatement]>), - Dts(Arc<[DefineTokenStatement]>), + Das(Arc<[DefineAccessStatement]>), Dus(Arc<[DefineUserStatement]>), Evs(Arc<[DefineEventStatement]>), Fcs(Arc<[DefineFunctionStatement]>), @@ -41,11 +40,9 @@ pub enum Entry { Lvs(Arc<[LiveStatement]>), Mls(Arc<[DefineModelStatement]>), Nss(Arc<[DefineNamespaceStatement]>), - Nts(Arc<[DefineTokenStatement]>), + Nas(Arc<[DefineAccessStatement]>), Nus(Arc<[DefineUserStatement]>), Pas(Arc<[DefineParamStatement]>), - Scs(Arc<[DefineScopeStatement]>), - Sts(Arc<[DefineTokenStatement]>), Tbs(Arc<[DefineTableStatement]>), // Sequences Seq(U32), diff --git a/core/src/kvs/ds.rs b/core/src/kvs/ds.rs index f4f47b08..7abf025b 100644 --- a/core/src/kvs/ds.rs +++ b/core/src/kvs/ds.rs @@ -1311,7 +1311,7 @@ impl Datastore { /// Evaluates a SQL [`Value`] without checking authenticating config /// This is used in very specific cases, where we do not need to check /// 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. /// /// ```rust,no_run diff --git a/core/src/kvs/lq_v2_doc.rs b/core/src/kvs/lq_v2_doc.rs index 26d67370..6becae6a 100644 --- a/core/src/kvs/lq_v2_doc.rs +++ b/core/src/kvs/lq_v2_doc.rs @@ -214,7 +214,7 @@ mod test_check_lqs_and_send_notifications { use crate::iam::{Auth, Role}; use crate::kvs::lq_v2_doc::construct_document; 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::{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 { let mut stm = LiveStatement::new(Fields::all()); let mut session: BTreeMap = 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_SCOPE.to_string(), Value::Strand(Strand::from("scope"))); session.insert(OBJ_PATH_TOKEN.to_string(), Value::Strand(Strand::from("token"))); let session = Value::Object(Object::from(session)); stm.session = Some(session); diff --git a/core/src/kvs/tx.rs b/core/src/kvs/tx.rs index c9b1a129..71872aa7 100644 --- a/core/src/kvs/tx.rs +++ b/core/src/kvs/tx.rs @@ -9,6 +9,7 @@ use futures::lock::Mutex; use uuid::Uuid; use sql::permission::Permissions; +use sql::statements::DefineAccessStatement; use sql::statements::DefineAnalyzerStatement; use sql::statements::DefineDatabaseStatement; use sql::statements::DefineEventStatement; @@ -18,9 +19,7 @@ use sql::statements::DefineIndexStatement; use sql::statements::DefineModelStatement; use sql::statements::DefineNamespaceStatement; use sql::statements::DefineParamStatement; -use sql::statements::DefineScopeStatement; use sql::statements::DefineTableStatement; -use sql::statements::DefineTokenStatement; use sql::statements::DefineUserStatement; use sql::statements::LiveStatement; @@ -1442,21 +1441,24 @@ impl Transaction { }) } - /// Retrieve all namespace token definitions for a specific namespace. - pub async fn all_ns_tokens(&mut self, ns: &str) -> Result, Error> { - let key = crate::key::namespace::tk::prefix(ns); + /// Retrieve all namespace access method definitions. + pub async fn all_ns_accesses( + &mut self, + ns: &str, + ) -> Result, Error> { + let key = crate::key::namespace::ac::prefix(ns); Ok(if let Some(e) = self.cache.get(&key) { - if let Entry::Nts(v) = e { + if let Entry::Nas(v) = e { v } else { unreachable!(); } } else { - let beg = crate::key::namespace::tk::prefix(ns); - let end = crate::key::namespace::tk::suffix(ns); + let beg = crate::key::namespace::ac::prefix(ns); + let end = crate::key::namespace::ac::suffix(ns); let val = self.getr(beg..end, u32::MAX).await?; let val = val.convert().into(); - self.cache.set(key, Entry::Nts(Arc::clone(&val))); + self.cache.set(key, Entry::Nas(Arc::clone(&val))); val }) } @@ -1503,25 +1505,25 @@ impl Transaction { }) } - /// Retrieve all database token definitions for a specific database. - pub async fn all_db_tokens( + /// Retrieve all database access method definitions. + pub async fn all_db_accesses( &mut self, ns: &str, db: &str, - ) -> Result, Error> { - let key = crate::key::database::tk::prefix(ns, db); + ) -> Result, Error> { + let key = crate::key::database::ac::prefix(ns, db); Ok(if let Some(e) = self.cache.get(&key) { - if let Entry::Dts(v) = e { + if let Entry::Das(v) = e { v } else { unreachable!(); } } else { - let beg = crate::key::database::tk::prefix(ns, db); - let end = crate::key::database::tk::suffix(ns, db); + let beg = crate::key::database::ac::prefix(ns, db); + let end = crate::key::database::ac::suffix(ns, db); let val = self.getr(beg..end, u32::MAX).await?; let val = val.convert().into(); - self.cache.set(key, Entry::Dts(Arc::clone(&val))); + self.cache.set(key, Entry::Das(Arc::clone(&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, 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, 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. pub async fn all_tb( &mut self, @@ -1863,15 +1818,15 @@ impl Transaction { Ok(val.into()) } - /// Retrieve a specific namespace token definition. - pub async fn get_ns_token( + /// Retrieve a specific namespace access method definition. + pub async fn get_ns_access( &mut self, ns: &str, - nt: &str, - ) -> Result { - let key = crate::key::namespace::tk::new(ns, nt); - let val = self.get(key).await?.ok_or(Error::NtNotFound { - value: nt.to_owned(), + ac: &str, + ) -> Result { + let key = crate::key::namespace::ac::new(ns, ac); + let val = self.get(key).await?.ok_or(Error::NaNotFound { + value: ac.to_owned(), })?; Ok(val.into()) } @@ -1916,16 +1871,16 @@ impl Transaction { Ok(val.into()) } - /// Retrieve a specific database token definition. - pub async fn get_db_token( + /// Retrieve a specific database access method definition. + pub async fn get_db_access( &mut self, ns: &str, db: &str, - dt: &str, - ) -> Result { - let key = crate::key::database::tk::new(ns, db, dt); - let val = self.get(key).await?.ok_or(Error::DtNotFound { - value: dt.to_owned(), + ac: &str, + ) -> Result { + let key = crate::key::database::ac::new(ns, db, ac); + let val = self.get(key).await?.ok_or(Error::DaNotFound { + value: ac.to_owned(), })?; Ok(val.into()) } @@ -1972,35 +1927,6 @@ impl Transaction { Ok(val.into()) } - /// Retrieve a specific scope definition. - pub async fn get_sc( - &mut self, - ns: &str, - db: &str, - sc: &str, - ) -> Result { - 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 { - 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 pub async fn get_lq( &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 { - 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. pub async fn add_tb( &mut self, @@ -2525,12 +2421,12 @@ impl Transaction { 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() { chn.send(bytes!("-- ------------------------------")).await?; - chn.send(bytes!("-- TOKENS")).await?; + chn.send(bytes!("-- ACCESSES")).await?; chn.send(bytes!("-- ------------------------------")).await?; chn.send(bytes!("")).await?; for dt in dts.iter() { @@ -2581,31 +2477,6 @@ impl Transaction { 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 { let tbs = self.all_tb(ns, db).await?; diff --git a/core/src/sql/access.rs b/core/src/sql/access.rs new file mode 100644 index 00000000..ecbf9c3a --- /dev/null +++ b/core/src/sql/access.rs @@ -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); + +impl From for Accesses { + fn from(v: Access) -> Self { + Accesses(vec![v]) + } +} + +impl Deref for Accesses { + type Target = Vec; + 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 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 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) + } +} diff --git a/core/src/sql/access_type.rs b/core/src/sql/access_type.rs new file mode 100644 index 00000000..4fe6921e --- /dev/null +++ b/core/src/sql/access_type.rs @@ -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, +} + +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, +} + +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, + pub signup: Option, + pub signin: Option, + 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) + } +} diff --git a/core/src/sql/algorithm.rs b/core/src/sql/algorithm.rs index b4286e40..36922d64 100644 --- a/core/src/sql/algorithm.rs +++ b/core/src/sql/algorithm.rs @@ -22,7 +22,33 @@ pub enum Algorithm { Rs256, Rs384, 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 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 { @@ -47,7 +73,6 @@ impl fmt::Display for Algorithm { Self::Rs256 => "RS256", Self::Rs384 => "RS384", Self::Rs512 => "RS512", - Self::Jwks => "JWKS", // Not an algorithm. }) } } diff --git a/core/src/sql/base.rs b/core/src/sql/base.rs index 30c5f9b5..725cc61b 100644 --- a/core/src/sql/base.rs +++ b/core/src/sql/base.rs @@ -12,6 +12,7 @@ pub enum Base { Root, Ns, Db, + // TODO(gguillemas): This variant is kept in 2.0.0 for backward compatibility. Drop in 3.0.0. Sc(Ident), } @@ -26,6 +27,7 @@ impl fmt::Display for Base { match self { Self::Ns => f.write_str("NAMESPACE"), 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::Root => f.write_str("ROOT"), } diff --git a/core/src/sql/mod.rs b/core/src/sql/mod.rs index 95eb68c3..34b9c0f0 100644 --- a/core/src/sql/mod.rs +++ b/core/src/sql/mod.rs @@ -1,5 +1,7 @@ //! The full type definitions for the SurrealQL query language +pub(crate) mod access; +pub(crate) mod access_type; pub(crate) mod algorithm; #[cfg(feature = "arbitrary")] pub(crate) mod arbitrary; @@ -74,6 +76,9 @@ pub mod index; pub mod serde; 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::array::Array; pub use self::base::Base; diff --git a/core/src/sql/paths.rs b/core/src/sql/paths.rs index 2c02782f..f3262916 100644 --- a/core/src/sql/paths.rs +++ b/core/src/sql/paths.rs @@ -1,9 +1,10 @@ use crate::sql::part::Part; use once_cell::sync::Lazy; -pub const OBJ_PATH_AUTH: &str = "sd"; -pub const OBJ_PATH_SCOPE: &str = "sc"; +pub const OBJ_PATH_ACCESS: &str = "ac"; +pub const OBJ_PATH_AUTH: &str = "rd"; pub const OBJ_PATH_TOKEN: &str = "tk"; + pub static ID: Lazy<[Part; 1]> = Lazy::new(|| [Part::from("id")]); 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 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")]); diff --git a/core/src/sql/statements/define/token.rs b/core/src/sql/statements/define/access.rs similarity index 57% rename from core/src/sql/statements/define/token.rs rename to core/src/sql/statements/define/access.rs index 6c1d5544..0784635a 100644 --- a/core/src/sql/statements/define/token.rs +++ b/core/src/sql/statements/define/access.rs @@ -4,27 +4,37 @@ use crate::doc::CursorDoc; use crate::err::Error; use crate::iam::{Action, ResourceKind}; 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 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)] +#[derive(Clone, Default, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] -pub struct DefineTokenStatement { +pub struct DefineAccessStatement { pub name: Ident, pub base: Base, - pub kind: Algorithm, - pub code: String, + pub kind: AccessType, pub comment: Option, #[revision(start = 2)] 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::() + } +} + +impl DefineAccessStatement { /// Process this type returning a computed simple Value pub(crate) async fn compute( &self, @@ -41,18 +51,18 @@ impl DefineTokenStatement { let mut run = txn.lock().await; // Clear the cache run.clear_cache(); - // Check if token already exists - if self.if_not_exists && run.get_ns_token(opt.ns(), &self.name).await.is_ok() { - return Err(Error::NtAlreadyExists { + // Check if access method already exists + if self.if_not_exists && run.get_ns_access(opt.ns(), &self.name).await.is_ok() { + return Err(Error::AccessNsAlreadyExists { value: self.name.to_string(), }); } // 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.set( key, - DefineTokenStatement { + DefineAccessStatement { if_not_exists: false, ..self.clone() }, @@ -66,50 +76,21 @@ impl DefineTokenStatement { let mut run = txn.lock().await; // Clear the cache run.clear_cache(); - // Check if token already exists + // Check if access method already 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(), }); } // 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_db(opt.ns(), opt.db(), opt.strict).await?; run.set( key, - DefineTokenStatement { - 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 { + DefineAccessStatement { if_not_exists: false, ..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 { - write!(f, "DEFINE TOKEN",)?; + write!(f, "DEFINE ACCESS",)?; if self.if_not_exists { write!(f, " IF NOT EXISTS")? } - write!( - f, - " {} ON {} TYPE {} VALUE {}", - self.name, - self.base, - self.kind, - quote_str(&self.code) - )?; + write!(f, " {} ON {}", self.name, self.base)?; + match &self.kind { + AccessType::Jwt(ac) => { + write!(f, " TYPE JWT {}", ac)?; + } + AccessType::Record(ac) => { + 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 { write!(f, " COMMENT {v}")? } @@ -145,13 +137,12 @@ impl Display for DefineTokenStatement { } } -impl InfoStructure for DefineTokenStatement { +impl InfoStructure for DefineAccessStatement { fn structure(self) -> Value { let Self { name, base, kind, - code, comment, .. } = self; @@ -163,8 +154,6 @@ impl InfoStructure for DefineTokenStatement { acc.insert("kind".to_string(), kind.structure()); - acc.insert("code".to_string(), code.into()); - if let Some(comment) = comment { acc.insert("comment".to_string(), comment.into()); } diff --git a/core/src/sql/statements/define/mod.rs b/core/src/sql/statements/define/mod.rs index f7f4a485..02e8c94f 100644 --- a/core/src/sql/statements/define/mod.rs +++ b/core/src/sql/statements/define/mod.rs @@ -1,3 +1,4 @@ +mod access; mod analyzer; mod database; mod event; @@ -7,11 +8,10 @@ mod index; mod model; mod namespace; mod param; -mod scope; mod table; -mod token; mod user; +pub use access::DefineAccessStatement; pub use analyzer::DefineAnalyzerStatement; pub use database::DefineDatabaseStatement; pub use event::DefineEventStatement; @@ -21,9 +21,7 @@ pub use index::DefineIndexStatement; pub use model::DefineModelStatement; pub use namespace::DefineNamespaceStatement; pub use param::DefineParamStatement; -pub use scope::DefineScopeStatement; pub use table::DefineTableStatement; -pub use token::DefineTokenStatement; pub use user::DefineUserStatement; use crate::ctx::Context; @@ -47,8 +45,6 @@ pub enum DefineStatement { Database(DefineDatabaseStatement), Function(DefineFunctionStatement), Analyzer(DefineAnalyzerStatement), - Token(DefineTokenStatement), - Scope(DefineScopeStatement), Param(DefineParamStatement), Table(DefineTableStatement), Event(DefineEventStatement), @@ -56,6 +52,7 @@ pub enum DefineStatement { Index(DefineIndexStatement), User(DefineUserStatement), Model(DefineModelStatement), + Access(DefineAccessStatement), } impl DefineStatement { @@ -76,8 +73,6 @@ impl DefineStatement { Self::Namespace(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::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::Table(ref v) => v.compute(stk, 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::User(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::Function(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::Table(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::Analyzer(v) => Display::fmt(v, f), Self::Model(v) => Display::fmt(v, f), + Self::Access(v) => Display::fmt(v, f), } } } diff --git a/core/src/sql/statements/define/scope.rs b/core/src/sql/statements/define/scope.rs deleted file mode 100644 index 8b846133..00000000 --- a/core/src/sql/statements/define/scope.rs +++ /dev/null @@ -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, - pub signup: Option, - pub signin: Option, - pub comment: Option, - #[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::() - } -} - -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 { - // 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) - } -} diff --git a/core/src/sql/statements/info.rs b/core/src/sql/statements/info.rs index f7d87bbb..dfe911e1 100644 --- a/core/src/sql/statements/info.rs +++ b/core/src/sql/statements/info.rs @@ -27,10 +27,6 @@ pub enum InfoStatement { Db, #[revision(start = 2)] Db(bool), - #[revision(end = 2, convert_fn = "sc_migrate")] - Sc(Ident), - #[revision(start = 2)] - Sc(Ident, bool), #[revision(end = 2, convert_fn = "tb_migrate")] Tb(Ident), #[revision(start = 2)] @@ -54,10 +50,6 @@ impl InfoStatement { Ok(Self::Db(false)) } - fn sc_migrate(_revision: u16, i: (Ident,)) -> Result { - Ok(Self::Sc(i.0, false)) - } - fn tb_migrate(_revision: u16, n: (Ident,)) -> Result { Ok(Self::Tb(n.0, false)) } @@ -122,12 +114,12 @@ impl InfoStatement { tmp.insert(v.name.to_string(), v.to_string().into()); } res.insert("users".to_owned(), tmp.into()); - // Process the tokens + // Process the accesses 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()); } - res.insert("tokens".to_owned(), tmp.into()); + res.insert("accesses".to_owned(), tmp.into()); // Ok all good Value::from(res).ok() } @@ -144,12 +136,6 @@ impl InfoStatement { tmp.insert(v.name.to_string(), v.to_string().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 let mut tmp = Object::default(); 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()); } res.insert("params".to_owned(), tmp.into()); - // Process the scopes + // Process the accesses 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()); } - res.insert("scopes".to_owned(), tmp.into()); + res.insert("accesses".to_owned(), tmp.into()); // Process the tables let mut tmp = Object::default(); for v in run.all_tb(opt.ns(), opt.db()).await?.iter() { @@ -189,22 +175,6 @@ impl InfoStatement { // Ok all good 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) => { // Allowed to run? 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?)); // Process the users res.insert("users".to_owned(), process_arr(run.all_ns_users(opt.ns()).await?)); - // Process the tokens - res.insert("tokens".to_owned(), process_arr(run.all_ns_tokens(opt.ns()).await?)); + // Process the accesses + res.insert( + "accesses".to_owned(), + process_arr(run.all_ns_accesses(opt.ns()).await?), + ); // Ok all good Value::from(res).ok() } @@ -304,10 +277,10 @@ impl InfoStatement { "users".to_owned(), process_arr(run.all_db_users(opt.ns(), opt.db()).await?), ); - // Process the tokens + // Process the accesses res.insert( - "tokens".to_owned(), - process_arr(run.all_db_tokens(opt.ns(), opt.db()).await?), + "accesses".to_owned(), + process_arr(run.all_db_accesses(opt.ns(), opt.db()).await?), ); // Process the functions res.insert( @@ -324,8 +297,11 @@ impl InfoStatement { "params".to_owned(), process_arr(run.all_db_params(opt.ns(), opt.db()).await?), ); - // Process the scopes - res.insert("scopes".to_owned(), process_arr(run.all_sc(opt.ns(), opt.db()).await?)); + // Process the accesses + res.insert( + "accesses".to_owned(), + process_arr(run.all_db_accesses(opt.ns(), opt.db()).await?), + ); // Process the tables res.insert("tables".to_owned(), process_arr(run.all_tb(opt.ns(), opt.db()).await?)); // Process the analyzers @@ -336,30 +312,6 @@ impl InfoStatement { // Ok all good 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) => { // Allowed to run? 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::Db(false) => f.write_str("INFO FOR DATABASE"), 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, true) => write!(f, "INFO FOR TABLE {t} STRUCTURE"), Self::User(ref u, ref b, false) => match b { @@ -453,7 +403,6 @@ impl InfoStatement { InfoStatement::Root(_) => InfoStatement::Root(true), InfoStatement::Ns(_) => InfoStatement::Ns(true), InfoStatement::Db(_) => InfoStatement::Db(true), - InfoStatement::Sc(s, _) => InfoStatement::Sc(s, true), InfoStatement::Tb(t, _) => InfoStatement::Tb(t, true), InfoStatement::User(u, b, _) => InfoStatement::User(u, b, true), } diff --git a/core/src/sql/statements/mod.rs b/core/src/sql/statements/mod.rs index 5b06222b..1add34bd 100644 --- a/core/src/sql/statements/mod.rs +++ b/core/src/sql/statements/mod.rs @@ -52,15 +52,15 @@ pub use self::throw::ThrowStatement; pub use self::update::UpdateStatement; pub use self::define::{ - DefineAnalyzerStatement, DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement, - DefineFunctionStatement, DefineIndexStatement, DefineModelStatement, DefineNamespaceStatement, - DefineParamStatement, DefineScopeStatement, DefineStatement, DefineTableStatement, - DefineTokenStatement, DefineUserStatement, + DefineAccessStatement, DefineAnalyzerStatement, DefineDatabaseStatement, DefineEventStatement, + DefineFieldStatement, DefineFunctionStatement, DefineIndexStatement, DefineModelStatement, + DefineNamespaceStatement, DefineParamStatement, DefineStatement, DefineTableStatement, + DefineUserStatement, }; pub use self::remove::{ - RemoveAnalyzerStatement, RemoveDatabaseStatement, RemoveEventStatement, RemoveFieldStatement, - RemoveFunctionStatement, RemoveIndexStatement, RemoveModelStatement, RemoveNamespaceStatement, - RemoveParamStatement, RemoveScopeStatement, RemoveStatement, RemoveTableStatement, - RemoveTokenStatement, RemoveUserStatement, + RemoveAccessStatement, RemoveAnalyzerStatement, RemoveDatabaseStatement, RemoveEventStatement, + RemoveFieldStatement, RemoveFunctionStatement, RemoveIndexStatement, RemoveModelStatement, + RemoveNamespaceStatement, RemoveParamStatement, RemoveStatement, RemoveTableStatement, + RemoveUserStatement, }; diff --git a/core/src/sql/statements/remove/token.rs b/core/src/sql/statements/remove/access.rs similarity index 66% rename from core/src/sql/statements/remove/token.rs rename to core/src/sql/statements/remove/access.rs index 75958115..96abb719 100644 --- a/core/src/sql/statements/remove/token.rs +++ b/core/src/sql/statements/remove/access.rs @@ -12,14 +12,14 @@ use std::fmt::{self, Display, Formatter}; #[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)] #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[non_exhaustive] -pub struct RemoveTokenStatement { +pub struct RemoveAccessStatement { pub name: Ident, pub base: Base, #[revision(start = 2)] pub if_exists: bool, } -impl RemoveTokenStatement { +impl RemoveAccessStatement { /// Process this type returning a computed simple Value pub(crate) async fn compute( &self, @@ -38,9 +38,9 @@ impl RemoveTokenStatement { // Clear the cache run.clear_cache(); // 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 - 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?; // Ok all good Ok(Value::None) @@ -51,22 +51,9 @@ impl RemoveTokenStatement { // Clear the cache run.clear_cache(); // 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 - let key = crate::key::database::tk::new(opt.ns(), opt.db(), &tk.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); + let key = crate::key::database::ac::new(opt.ns(), opt.db(), &ac.name); run.del(key).await?; // Ok all good Ok(Value::None) @@ -77,13 +64,10 @@ impl RemoveTokenStatement { .await; match future { Err(e) if self.if_exists => match e { - Error::NtNotFound { + Error::NaNotFound { .. } => Ok(Value::None), - Error::DtNotFound { - .. - } => Ok(Value::None), - Error::StNotFound { + Error::DaNotFound { .. } => Ok(Value::None), 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 { - write!(f, "REMOVE TOKEN")?; + write!(f, "REMOVE ACCESS")?; if self.if_exists { write!(f, " IF EXISTS")? } diff --git a/core/src/sql/statements/remove/mod.rs b/core/src/sql/statements/remove/mod.rs index 477dd26c..3ca18e5d 100644 --- a/core/src/sql/statements/remove/mod.rs +++ b/core/src/sql/statements/remove/mod.rs @@ -1,3 +1,4 @@ +mod access; mod analyzer; mod database; mod event; @@ -7,11 +8,10 @@ mod index; mod model; mod namespace; mod param; -mod scope; mod table; -mod token; mod user; +pub use access::RemoveAccessStatement; pub use analyzer::RemoveAnalyzerStatement; pub use database::RemoveDatabaseStatement; pub use event::RemoveEventStatement; @@ -21,9 +21,7 @@ pub use index::RemoveIndexStatement; pub use model::RemoveModelStatement; pub use namespace::RemoveNamespaceStatement; pub use param::RemoveParamStatement; -pub use scope::RemoveScopeStatement; pub use table::RemoveTableStatement; -pub use token::RemoveTokenStatement; pub use user::RemoveUserStatement; use crate::ctx::Context; @@ -45,8 +43,7 @@ pub enum RemoveStatement { Database(RemoveDatabaseStatement), Function(RemoveFunctionStatement), Analyzer(RemoveAnalyzerStatement), - Token(RemoveTokenStatement), - Scope(RemoveScopeStatement), + Access(RemoveAccessStatement), Param(RemoveParamStatement), Table(RemoveTableStatement), Event(RemoveEventStatement), @@ -73,8 +70,7 @@ impl RemoveStatement { Self::Namespace(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::Token(ref v) => v.compute(ctx, opt, txn).await, - Self::Scope(ref v) => v.compute(ctx, opt, txn).await, + Self::Access(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::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::Database(v) => Display::fmt(v, f), Self::Function(v) => Display::fmt(v, f), - Self::Token(v) => Display::fmt(v, f), - Self::Scope(v) => Display::fmt(v, f), + Self::Access(v) => Display::fmt(v, f), Self::Param(v) => Display::fmt(v, f), Self::Table(v) => Display::fmt(v, f), Self::Event(v) => Display::fmt(v, f), diff --git a/core/src/sql/statements/remove/scope.rs b/core/src/sql/statements/remove/scope.rs deleted file mode 100644 index 3ea8ec4d..00000000 --- a/core/src/sql/statements/remove/scope.rs +++ /dev/null @@ -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 { - 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(()) - } -} diff --git a/core/src/sql/value/serde/ser/access/mod.rs b/core/src/sql/value/serde/ser/access/mod.rs new file mode 100644 index 00000000..9b98c1a6 --- /dev/null +++ b/core/src/sql/value/serde/ser/access/mod.rs @@ -0,0 +1,2 @@ +pub(super) mod opt; +pub(super) mod vec; diff --git a/core/src/sql/value/serde/ser/access/opt.rs b/core/src/sql/value/serde/ser/access/opt.rs new file mode 100644 index 00000000..1f106f88 --- /dev/null +++ b/core/src/sql/value/serde/ser/access/opt.rs @@ -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; + type Error = Error; + + type SerializeSeq = Impossible, Error>; + type SerializeTuple = Impossible, Error>; + type SerializeTupleStruct = Impossible, Error>; + type SerializeTupleVariant = Impossible, Error>; + type SerializeMap = Impossible, Error>; + type SerializeStruct = Impossible, Error>; + type SerializeStructVariant = Impossible, Error>; + + const EXPECTED: &'static str = "an `Option`"; + + #[inline] + fn serialize_none(self) -> Result { + Ok(None) + } + + #[inline] + fn serialize_some(self, value: &T) -> Result + 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 = 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); + } +} diff --git a/core/src/sql/value/serde/ser/access/vec.rs b/core/src/sql/value/serde/ser/access/vec.rs new file mode 100644 index 00000000..e1a5ad13 --- /dev/null +++ b/core/src/sql/value/serde/ser/access/vec.rs @@ -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; + type Error = Error; + + type SerializeSeq = SerializeAccessVec; + type SerializeTuple = Impossible, Error>; + type SerializeTupleStruct = Impossible, Error>; + type SerializeTupleVariant = Impossible, Error>; + type SerializeMap = Impossible, Error>; + type SerializeStruct = Impossible, Error>; + type SerializeStructVariant = Impossible, Error>; + + const EXPECTED: &'static str = "a `Access` sequence"; + + fn serialize_seq(self, len: Option) -> Result { + Ok(SerializeAccessVec(Vec::with_capacity(len.unwrap_or_default()))) + } + + #[inline] + fn serialize_newtype_struct( + self, + _name: &'static str, + value: &T, + ) -> Result + where + T: ?Sized + Serialize, + { + value.serialize(self.wrap()) + } +} + +#[non_exhaustive] +pub struct SerializeAccessVec(Vec); + +impl serde::ser::SerializeSeq for SerializeAccessVec { + type Ok = Vec; + type Error = Error; + + fn serialize_element(&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 { + Ok(self.0) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty() { + let vec: Vec = 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); + } +} diff --git a/core/src/sql/value/serde/ser/access_type/mod.rs b/core/src/sql/value/serde/ser/access_type/mod.rs new file mode 100644 index 00000000..452de616 --- /dev/null +++ b/core/src/sql/value/serde/ser/access_type/mod.rs @@ -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; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + const EXPECTED: &'static str = "a `AccessType`"; + + fn serialize_newtype_variant( + self, + name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result + 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; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = SerializeRecord; + type SerializeStructVariant = Impossible; + + const EXPECTED: &'static str = "a struct `RecordAccess`"; + + #[inline] + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(SerializeRecord::default()) + } +} + +#[derive(Default)] +#[non_exhaustive] +pub struct SerializeRecord { + pub duration: Option, + pub signup: Option, + pub signin: Option, + pub jwt: JwtAccess, +} + +impl serde::ser::SerializeStruct for SerializeRecord { + type Ok = RecordAccess; + type Error = Error; + + fn serialize_field(&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 { + 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; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = SerializeJwt; + type SerializeStructVariant = Impossible; + + const EXPECTED: &'static str = "a struct `JwtAccess`"; + + #[inline] + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(SerializeJwt::default()) + } +} + +#[derive(Default)] +#[non_exhaustive] +pub struct SerializeJwt { + pub verify: JwtAccessVerify, + pub issue: Option, +} + +impl serde::ser::SerializeStruct for SerializeJwt { + type Ok = JwtAccess; + type Error = Error; + + fn serialize_field(&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 { + 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; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Impossible; + type SerializeStructVariant = Impossible; + + const EXPECTED: &'static str = "a `JwtAccessVerify`"; + + fn serialize_newtype_variant( + self, + name: &'static str, + _variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result + 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; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = SerializeJwtVerifyKey; + type SerializeStructVariant = Impossible; + + const EXPECTED: &'static str = "a struct `JwtAccessVerifyKey`"; + + #[inline] + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + 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(&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 { + 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; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = SerializeJwtVerifyJwks; + type SerializeStructVariant = Impossible; + + const EXPECTED: &'static str = "a struct `JwtAccessVerifyJwks`"; + + #[inline] + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + 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(&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 { + Ok(JwtAccessVerifyJwks { + url: self.url, + }) + } +} + +// Serialize JWT Access Issue + +pub struct SerializerJwtIssueOpt; + +impl ser::Serializer for SerializerJwtIssueOpt { + type Ok = Option; + type Error = Error; + + type SerializeSeq = Impossible, Error>; + type SerializeTuple = Impossible, Error>; + type SerializeTupleStruct = Impossible, Error>; + type SerializeTupleVariant = Impossible, Error>; + type SerializeMap = Impossible, Error>; + type SerializeStruct = Impossible, Error>; + type SerializeStructVariant = Impossible, Error>; + + const EXPECTED: &'static str = "an `Option`"; + + #[inline] + fn serialize_none(self) -> Result { + Ok(None) + } + + #[inline] + fn serialize_some(self, value: &T) -> Result + 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; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = SerializeJwtIssue; + type SerializeStructVariant = Impossible; + + const EXPECTED: &'static str = "a struct `JwtAccessIssue`"; + + #[inline] + fn serialize_struct( + self, + _name: &'static str, + _len: usize, + ) -> Result { + Ok(SerializeJwtIssue::default()) + } +} + +#[derive(Default)] +#[non_exhaustive] +pub struct SerializeJwtIssue { + pub alg: Algorithm, + pub key: String, + pub duration: Option, +} + +impl serde::ser::SerializeStruct for SerializeJwtIssue { + type Ok = JwtAccessIssue; + type Error = Error; + + fn serialize_field(&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 { + Ok(JwtAccessIssue { + duration: self.duration, + alg: self.alg, + key: self.key, + }) + } +} diff --git a/core/src/sql/value/serde/ser/mod.rs b/core/src/sql/value/serde/ser/mod.rs index e435330b..b01a0ad6 100644 --- a/core/src/sql/value/serde/ser/mod.rs +++ b/core/src/sql/value/serde/ser/mod.rs @@ -1,3 +1,4 @@ +mod access_type; mod algorithm; mod base; mod block; diff --git a/core/src/sql/value/serde/ser/statement/define/token.rs b/core/src/sql/value/serde/ser/statement/define/access.rs similarity index 55% rename from core/src/sql/value/serde/ser/statement/define/token.rs rename to core/src/sql/value/serde/ser/statement/define/access.rs index 26ec5491..7acacb2a 100644 --- a/core/src/sql/value/serde/ser/statement/define/token.rs +++ b/core/src/sql/value/serde/ser/statement/define/access.rs @@ -1,7 +1,7 @@ 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::Algorithm; use crate::sql::Base; use crate::sql::Ident; use crate::sql::Strand; @@ -14,18 +14,18 @@ use serde::ser::Serialize; pub struct Serializer; impl ser::Serializer for Serializer { - type Ok = DefineTokenStatement; + type Ok = DefineAccessStatement; type Error = Error; - type SerializeSeq = Impossible; - type SerializeTuple = Impossible; - type SerializeTupleStruct = Impossible; - type SerializeTupleVariant = Impossible; - type SerializeMap = Impossible; - type SerializeStruct = SerializeDefineTokenStatement; - type SerializeStructVariant = Impossible; + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = SerializeDefineAccessStatement; + type SerializeStructVariant = Impossible; - const EXPECTED: &'static str = "a struct `DefineTokenStatement`"; + const EXPECTED: &'static str = "a struct `DefineAccessStatement`"; #[inline] fn serialize_struct( @@ -33,23 +33,22 @@ impl ser::Serializer for Serializer { _name: &'static str, _len: usize, ) -> Result { - Ok(SerializeDefineTokenStatement::default()) + Ok(SerializeDefineAccessStatement::default()) } } #[derive(Default)] #[non_exhaustive] -pub struct SerializeDefineTokenStatement { +pub struct SerializeDefineAccessStatement { name: Ident, base: Base, - kind: Algorithm, - code: String, + kind: AccessType, comment: Option, if_not_exists: bool, } -impl serde::ser::SerializeStruct for SerializeDefineTokenStatement { - type Ok = DefineTokenStatement; +impl serde::ser::SerializeStruct for SerializeDefineAccessStatement { + type Ok = DefineAccessStatement; type Error = Error; fn serialize_field(&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())?; } "kind" => { - self.kind = value.serialize(ser::algorithm::Serializer.wrap())?; - } - "code" => { - self.code = value.serialize(ser::string::Serializer.wrap())?; + self.kind = value.serialize(ser::access_type::Serializer.wrap())?; } "comment" => { self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?; @@ -77,7 +73,7 @@ impl serde::ser::SerializeStruct for SerializeDefineTokenStatement { } key => { 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 { - Ok(DefineTokenStatement { + Ok(DefineAccessStatement { name: self.name, base: self.base, kind: self.kind, - code: self.code, comment: self.comment, if_not_exists: self.if_not_exists, }) @@ -102,8 +97,8 @@ mod tests { #[test] fn default() { - let stmt = DefineTokenStatement::default(); - let value: DefineTokenStatement = stmt.serialize(Serializer.wrap()).unwrap(); + let stmt = DefineAccessStatement::default(); + let value: DefineAccessStatement = stmt.serialize(Serializer.wrap()).unwrap(); assert_eq!(value, stmt); } } diff --git a/core/src/sql/value/serde/ser/statement/define/mod.rs b/core/src/sql/value/serde/ser/statement/define/mod.rs index 52413c04..d063c6fd 100644 --- a/core/src/sql/value/serde/ser/statement/define/mod.rs +++ b/core/src/sql/value/serde/ser/statement/define/mod.rs @@ -1,3 +1,4 @@ +mod access; mod analyzer; mod database; mod event; @@ -6,9 +7,7 @@ mod function; mod index; mod namespace; mod param; -mod scope; mod table; -mod token; mod user; use crate::err::Error; @@ -59,8 +58,7 @@ impl ser::Serializer for Serializer { "Analyzer" => { Ok(DefineStatement::Analyzer(value.serialize(analyzer::Serializer.wrap())?)) } - "Token" => Ok(DefineStatement::Token(value.serialize(token::Serializer.wrap())?)), - "Scope" => Ok(DefineStatement::Scope(value.serialize(scope::Serializer.wrap())?)), + "Access" => Ok(DefineStatement::Access(value.serialize(access::Serializer.wrap())?)), "Param" => Ok(DefineStatement::Param(value.serialize(param::Serializer.wrap())?)), "Table" => Ok(DefineStatement::Table(value.serialize(table::Serializer.wrap())?)), "Event" => Ok(DefineStatement::Event(value.serialize(event::Serializer.wrap())?)), @@ -108,15 +106,8 @@ mod tests { } #[test] - fn token() { - let stmt = DefineStatement::Token(Default::default()); - let serialized = stmt.serialize(Serializer.wrap()).unwrap(); - assert_eq!(stmt, serialized); - } - - #[test] - fn scope() { - let stmt = DefineStatement::Scope(Default::default()); + fn access() { + let stmt = DefineStatement::Access(Default::default()); let serialized = stmt.serialize(Serializer.wrap()).unwrap(); assert_eq!(stmt, serialized); } diff --git a/core/src/sql/value/serde/ser/statement/define/scope.rs b/core/src/sql/value/serde/ser/statement/define/scope.rs deleted file mode 100644 index aca035ed..00000000 --- a/core/src/sql/value/serde/ser/statement/define/scope.rs +++ /dev/null @@ -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; - type SerializeTuple = Impossible; - type SerializeTupleStruct = Impossible; - type SerializeTupleVariant = Impossible; - type SerializeMap = Impossible; - type SerializeStruct = SerializeDefineScopeStatement; - type SerializeStructVariant = Impossible; - - const EXPECTED: &'static str = "a struct `DefineScopeStatement`"; - - #[inline] - fn serialize_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - Ok(SerializeDefineScopeStatement::default()) - } -} - -#[derive(Default)] -#[non_exhaustive] -pub struct SerializeDefineScopeStatement { - name: Ident, - code: String, - session: Option, - signup: Option, - signin: Option, - comment: Option, - if_not_exists: bool, -} - -impl serde::ser::SerializeStruct for SerializeDefineScopeStatement { - type Ok = DefineScopeStatement; - type Error = Error; - - fn serialize_field(&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 { - 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); - } -} diff --git a/core/src/sql/value/serde/ser/statement/info.rs b/core/src/sql/value/serde/ser/statement/info.rs index dfe2c7ca..9624879b 100644 --- a/core/src/sql/value/serde/ser/statement/info.rs +++ b/core/src/sql/value/serde/ser/statement/info.rs @@ -61,7 +61,6 @@ impl ser::Serializer for Serializer { _len: usize, ) -> Result { match variant { - "Sc" => Ok(SerializeInfoStatement::with(Which::Sc)), "Tb" => Ok(SerializeInfoStatement::with(Which::Tb)), "User" => Ok(SerializeInfoStatement::with(Which::User)), variant => Err(Error::custom(format!("unexpected tuple variant `{name}::{variant}`"))), @@ -71,7 +70,6 @@ impl ser::Serializer for Serializer { #[derive(Clone, Copy)] enum Which { - Sc, Tb, User, } @@ -79,9 +77,6 @@ enum Which { impl Display for Which { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Which::Sc => { - write!(f, "Sc") - } Which::Tb => { write!(f, "Tb") } @@ -121,7 +116,7 @@ impl serde::ser::SerializeTupleVariant for SerializeInfoStatement { (_, 0) => { 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())?; } (User, 1) => { @@ -144,8 +139,6 @@ impl serde::ser::SerializeTupleVariant for SerializeInfoStatement { fn end(self) -> Result { use Which::*; 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, None) => Err(Error::custom("`InfoStatement::Tb` missing required value(s)")), (User, Some(ident)) => Ok(InfoStatement::User(ident, self.tuple.1, self.tuple.2)), @@ -179,13 +172,6 @@ mod tests { 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] fn tb() { let stmt = InfoStatement::Tb(Default::default(), Default::default()); diff --git a/core/src/sql/value/serde/ser/statement/remove/token.rs b/core/src/sql/value/serde/ser/statement/remove/access.rs similarity index 56% rename from core/src/sql/value/serde/ser/statement/remove/token.rs rename to core/src/sql/value/serde/ser/statement/remove/access.rs index 8dfe6d3d..e3a78511 100644 --- a/core/src/sql/value/serde/ser/statement/remove/token.rs +++ b/core/src/sql/value/serde/ser/statement/remove/access.rs @@ -1,5 +1,5 @@ use crate::err::Error; -use crate::sql::statements::RemoveTokenStatement; +use crate::sql::statements::RemoveAccessStatement; use crate::sql::value::serde::ser; use crate::sql::Base; use crate::sql::Ident; @@ -12,18 +12,18 @@ use serde::ser::Serialize; pub struct Serializer; impl ser::Serializer for Serializer { - type Ok = RemoveTokenStatement; + type Ok = RemoveAccessStatement; type Error = Error; - type SerializeSeq = Impossible; - type SerializeTuple = Impossible; - type SerializeTupleStruct = Impossible; - type SerializeTupleVariant = Impossible; - type SerializeMap = Impossible; - type SerializeStruct = SerializeRemoveTokenStatement; - type SerializeStructVariant = Impossible; + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = SerializeRemoveAccessStatement; + type SerializeStructVariant = Impossible; - const EXPECTED: &'static str = "a struct `RemoveTokenStatement`"; + const EXPECTED: &'static str = "a struct `RemoveAccessStatement`"; #[inline] fn serialize_struct( @@ -31,20 +31,20 @@ impl ser::Serializer for Serializer { _name: &'static str, _len: usize, ) -> Result { - Ok(SerializeRemoveTokenStatement::default()) + Ok(SerializeRemoveAccessStatement::default()) } } #[derive(Default)] #[non_exhaustive] -pub struct SerializeRemoveTokenStatement { +pub struct SerializeRemoveAccessStatement { name: Ident, base: Base, if_exists: bool, } -impl serde::ser::SerializeStruct for SerializeRemoveTokenStatement { - type Ok = RemoveTokenStatement; +impl serde::ser::SerializeStruct for SerializeRemoveAccessStatement { + type Ok = RemoveAccessStatement; type Error = Error; fn serialize_field(&mut self, key: &'static str, value: &T) -> Result<(), Error> @@ -63,7 +63,7 @@ impl serde::ser::SerializeStruct for SerializeRemoveTokenStatement { } key => { 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 { - Ok(RemoveTokenStatement { + Ok(RemoveAccessStatement { name: self.name, base: self.base, if_exists: self.if_exists, @@ -85,8 +85,8 @@ mod tests { #[test] fn default() { - let stmt = RemoveTokenStatement::default(); - let value: RemoveTokenStatement = stmt.serialize(Serializer.wrap()).unwrap(); + let stmt = RemoveAccessStatement::default(); + let value: RemoveAccessStatement = stmt.serialize(Serializer.wrap()).unwrap(); assert_eq!(value, stmt); } } diff --git a/core/src/sql/value/serde/ser/statement/remove/mod.rs b/core/src/sql/value/serde/ser/statement/remove/mod.rs index 4d116944..f6569e02 100644 --- a/core/src/sql/value/serde/ser/statement/remove/mod.rs +++ b/core/src/sql/value/serde/ser/statement/remove/mod.rs @@ -1,3 +1,4 @@ +mod access; mod analyzer; mod database; mod event; @@ -6,9 +7,7 @@ mod function; mod index; mod namespace; mod param; -mod scope; mod table; -mod token; mod user; use crate::err::Error; @@ -59,8 +58,7 @@ impl ser::Serializer for Serializer { "Analyzer" => { Ok(RemoveStatement::Analyzer(value.serialize(analyzer::Serializer.wrap())?)) } - "Token" => Ok(RemoveStatement::Token(value.serialize(token::Serializer.wrap())?)), - "Scope" => Ok(RemoveStatement::Scope(value.serialize(scope::Serializer.wrap())?)), + "Access" => Ok(RemoveStatement::Access(value.serialize(access::Serializer.wrap())?)), "Param" => Ok(RemoveStatement::Param(value.serialize(param::Serializer.wrap())?)), "Table" => Ok(RemoveStatement::Table(value.serialize(table::Serializer.wrap())?)), "Event" => Ok(RemoveStatement::Event(value.serialize(event::Serializer.wrap())?)), @@ -108,15 +106,8 @@ mod tests { } #[test] - fn token() { - let stmt = RemoveStatement::Token(Default::default()); - let serialized = stmt.serialize(Serializer.wrap()).unwrap(); - assert_eq!(stmt, serialized); - } - - #[test] - fn scope() { - let stmt = RemoveStatement::Scope(Default::default()); + fn access() { + let stmt = RemoveStatement::Access(Default::default()); let serialized = stmt.serialize(Serializer.wrap()).unwrap(); assert_eq!(stmt, serialized); } diff --git a/core/src/sql/value/serde/ser/statement/remove/scope.rs b/core/src/sql/value/serde/ser/statement/remove/scope.rs deleted file mode 100644 index 66722953..00000000 --- a/core/src/sql/value/serde/ser/statement/remove/scope.rs +++ /dev/null @@ -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; - type SerializeTuple = Impossible; - type SerializeTupleStruct = Impossible; - type SerializeTupleVariant = Impossible; - type SerializeMap = Impossible; - type SerializeStruct = SerializeRemoveScopeStatement; - type SerializeStructVariant = Impossible; - - const EXPECTED: &'static str = "a struct `RemoveScopeStatement`"; - - #[inline] - fn serialize_struct( - self, - _name: &'static str, - _len: usize, - ) -> Result { - 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(&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 { - 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); - } -} diff --git a/core/src/syn/lexer/keywords.rs b/core/src/syn/lexer/keywords.rs index 54c7b154..10a7c067 100644 --- a/core/src/syn/lexer/keywords.rs +++ b/core/src/syn/lexer/keywords.rs @@ -56,7 +56,9 @@ pub fn could_be_reserved(s: &str) -> bool { /// A map for mapping keyword strings to a tokenkind, pub(crate) static KEYWORDS: phf::Map, TokenKind> = phf_map! { // Keywords + UniCase::ascii("ACCESS") => TokenKind::Keyword(Keyword::Access), UniCase::ascii("AFTER") => TokenKind::Keyword(Keyword::After), + UniCase::ascii("ALGORITHM") => TokenKind::Keyword(Keyword::Algorithm), UniCase::ascii("ALL") => TokenKind::Keyword(Keyword::All), UniCase::ascii("ANALYZE") => TokenKind::Keyword(Keyword::Analyze), UniCase::ascii("ANALYZER") => TokenKind::Keyword(Keyword::Analyzer), @@ -132,6 +134,9 @@ pub(crate) static KEYWORDS: phf::Map, TokenKind> = phf_map UniCase::ascii("INTO") => TokenKind::Keyword(Keyword::Into), UniCase::ascii("IF") => TokenKind::Keyword(Keyword::If), 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("KEEP_PRUNED_CONNECTIONS") => TokenKind::Keyword(Keyword::KeepPrunedConnections), UniCase::ascii("KILL") => TokenKind::Keyword(Keyword::Kill), @@ -214,6 +219,7 @@ pub(crate) static KEYWORDS: phf::Map, TokenKind> = phf_map UniCase::ascii("UNSET") => TokenKind::Keyword(Keyword::Unset), UniCase::ascii("UPDATE") => TokenKind::Keyword(Keyword::Update), UniCase::ascii("UPPERCASE") => TokenKind::Keyword(Keyword::Uppercase), + UniCase::ascii("URL") => TokenKind::Keyword(Keyword::Url), UniCase::ascii("USE") => TokenKind::Keyword(Keyword::Use), UniCase::ascii("USER") => TokenKind::Keyword(Keyword::User), UniCase::ascii("VALUE") => TokenKind::Keyword(Keyword::Value), @@ -338,7 +344,6 @@ pub(crate) static KEYWORDS: phf::Map, TokenKind> = phf_map UniCase::ascii("RS256") => TokenKind::Algorithm(Algorithm::Rs256), UniCase::ascii("RS384") => TokenKind::Algorithm(Algorithm::Rs384), UniCase::ascii("RS512") => TokenKind::Algorithm(Algorithm::Rs512), - UniCase::ascii("JWKS") => jwks_token_kind(), // Necessary because `phf_map!` doesn't support `cfg` attributes // Distance UniCase::ascii("CHEBYSHEV") => TokenKind::Distance(DistanceKind::Chebyshev), @@ -360,11 +365,3 @@ pub(crate) static KEYWORDS: phf::Map, TokenKind> = phf_map // Change Feed keywords 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 -} diff --git a/core/src/syn/parser/builtin.rs b/core/src/syn/parser/builtin.rs index 9ad4e2dd..d7080f31 100644 --- a/core/src/syn/parser/builtin.rs +++ b/core/src/syn/parser/builtin.rs @@ -226,8 +226,8 @@ pub(crate) static PATHS: phf::Map, PathKind> = phf_map! { UniCase::ascii("session::ip") => PathKind::Function, UniCase::ascii("session::ns") => PathKind::Function, UniCase::ascii("session::origin") => PathKind::Function, - UniCase::ascii("session::sc") => PathKind::Function, - UniCase::ascii("session::sd") => PathKind::Function, + UniCase::ascii("session::ac") => PathKind::Function, + UniCase::ascii("session::rd") => PathKind::Function, UniCase::ascii("session::token") => PathKind::Function, // UniCase::ascii("string::concat") => PathKind::Function, diff --git a/core/src/syn/parser/stmt/define.rs b/core/src/syn/parser/stmt/define.rs index 7f5ac955..9623fbc1 100644 --- a/core/src/syn/parser/stmt/define.rs +++ b/core/src/syn/parser/stmt/define.rs @@ -1,26 +1,30 @@ use reblessive::Stk; +use crate::sql::access_type::JwtAccessVerify; use crate::sql::index::HnswParams; use crate::{ sql::{ + access_type, + base::Base, filter::Filter, index::{Distance, VectorType}, statements::{ - DefineAnalyzerStatement, DefineDatabaseStatement, DefineEventStatement, - DefineFieldStatement, DefineFunctionStatement, DefineIndexStatement, - DefineNamespaceStatement, DefineParamStatement, DefineScopeStatement, DefineStatement, - DefineTableStatement, DefineTokenStatement, DefineUserStatement, + DefineAccessStatement, DefineAnalyzerStatement, DefineDatabaseStatement, + DefineEventStatement, DefineFieldStatement, DefineFunctionStatement, + DefineIndexStatement, DefineNamespaceStatement, DefineParamStatement, DefineStatement, + DefineTableStatement, DefineUserStatement, }, table_type, tokenizer::Tokenizer, - Ident, Idioms, Index, Kind, Param, Permissions, Scoring, Strand, TableType, Values, + AccessType, Ident, Idioms, Index, Kind, Param, Permissions, Scoring, Strand, TableType, + Values, }, syn::{ parser::{ mac::{expected, unexpected}, 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!("FUNCTION") => self.parse_define_function(ctx).await.map(DefineStatement::Function), t!("USER") => self.parse_define_user().map(DefineStatement::User), - t!("TOKEN") => self.parse_define_token().map(DefineStatement::Token), - t!("SCOPE") => self.parse_define_scope(ctx).await.map(DefineStatement::Scope), + t!("TOKEN") => self.parse_define_token().map(DefineStatement::Access), + t!("SCOPE") => self.parse_define_scope(ctx).await.map(DefineStatement::Access), t!("PARAM") => self.parse_define_param(ctx).await.map(DefineStatement::Param), t!("TABLE") => self.parse_define_table(ctx).await.map(DefineStatement::Table), t!("EVENT") => { @@ -43,6 +47,7 @@ impl Parser<'_> { } t!("INDEX") => self.parse_define_index().map(DefineStatement::Index), 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"), } } @@ -211,7 +216,10 @@ impl Parser<'_> { Ok(res) } - pub fn parse_define_token(&mut self) -> ParseResult { + pub async fn parse_define_access( + &mut self, + stk: &mut Stk, + ) -> ParseResult { let if_not_exists = if self.eat(t!("IF")) { expected!(self, t!("NOT")); expected!(self, t!("EXISTS")); @@ -221,9 +229,10 @@ impl Parser<'_> { }; let name = self.next_token_value()?; 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, base, if_not_exists, @@ -236,17 +245,49 @@ impl Parser<'_> { self.pop_peek(); res.comment = Some(self.next_token_value()?); } - t!("VALUE") => { - self.pop_peek(); - res.code = self.next_token_value::()?.0; - } t!("TYPE") => { self.pop_peek(); - match self.next().kind { - TokenKind::Algorithm(x) => { - res.kind = x; + match self.peek_kind() { + t!("JWT") => { + 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, @@ -256,7 +297,8 @@ impl Parser<'_> { Ok(res) } - pub async fn parse_define_scope(&mut self, stk: &mut Stk) -> ParseResult { + // 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 { let if_not_exists = if self.eat(t!("IF")) { expected!(self, t!("NOT")); expected!(self, t!("EXISTS")); @@ -265,13 +307,130 @@ impl Parser<'_> { false }; 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, - code: DefineScopeStatement::random_code(), + base: base.clone(), if_not_exists, ..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::()?.0, + }, + ); + } + TokenKind::Keyword(Keyword::Jwks) => { + expected!(self, t!("VALUE")); + ac.jwt.verify = access_type::JwtAccessVerify::Jwks( + access_type::JwtAccessVerifyJwks { + url: self.next_token_value::()?.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::()?.0, + }, + ); + } + TokenKind::Keyword(Keyword::Jwks) => { + expected!(self, t!("VALUE")); + ac.verify = access_type::JwtAccessVerify::Jwks( + access_type::JwtAccessVerifyJwks { + url: self.next_token_value::()?.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 { + 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 { match self.peek_kind() { t!("COMMENT") => { @@ -280,20 +439,26 @@ impl Parser<'_> { } t!("SESSION") => { 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") => { 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") => { 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, } } + res.kind = AccessType::Record(ac); + Ok(res) } @@ -914,4 +1079,110 @@ impl Parser<'_> { } Ok(Kind::Record(names)) } + + pub fn parse_jwt(&mut self, ac: Option) -> ParseResult { + 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::()?.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::()?.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::()?.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) + } } diff --git a/core/src/syn/parser/stmt/mod.rs b/core/src/syn/parser/stmt/mod.rs index 74c378db..31d987ca 100644 --- a/core/src/syn/parser/stmt/mod.rs +++ b/core/src/syn/parser/stmt/mod.rs @@ -438,10 +438,6 @@ impl Parser<'_> { t!("ROOT") => InfoStatement::Root(false), t!("NAMESPACE") => InfoStatement::Ns(false), t!("DATABASE") => InfoStatement::Db(false), - t!("SCOPE") => { - let ident = self.next_token_value()?; - InfoStatement::Sc(ident, false) - } t!("TABLE") => { let ident = self.next_token_value()?; InfoStatement::Tb(ident, false) diff --git a/core/src/syn/parser/stmt/parts.rs b/core/src/syn/parser/stmt/parts.rs index c0e3d36f..16cffe8a 100644 --- a/core/src/syn/parser/stmt/parts.rs +++ b/core/src/syn/parser/stmt/parts.rs @@ -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 /// - /// 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 /// Expects the next keyword to be a base. @@ -327,9 +328,9 @@ impl Parser<'_> { } x => { if scope_allowed { - unexpected!(self, x, "'NAMEPSPACE', 'DATABASE', 'ROOT', 'SCOPE' or 'KV'") + unexpected!(self, x, "'NAMEPSPACE', 'DATABASE', 'ROOT' or 'SCOPE'") } else { - unexpected!(self, x, "'NAMEPSPACE', 'DATABASE', 'ROOT', or 'KV'") + unexpected!(self, x, "'NAMEPSPACE', 'DATABASE' or 'ROOT'") } } } diff --git a/core/src/syn/parser/stmt/remove.rs b/core/src/syn/parser/stmt/remove.rs index 4a573cef..1aaa05aa 100644 --- a/core/src/syn/parser/stmt/remove.rs +++ b/core/src/syn/parser/stmt/remove.rs @@ -1,9 +1,9 @@ use crate::{ sql::{ statements::{ - remove::RemoveAnalyzerStatement, RemoveDatabaseStatement, RemoveEventStatement, - RemoveFieldStatement, RemoveFunctionStatement, RemoveIndexStatement, - RemoveNamespaceStatement, RemoveParamStatement, RemoveScopeStatement, RemoveStatement, + remove::RemoveAnalyzerStatement, RemoveAccessStatement, RemoveDatabaseStatement, + RemoveEventStatement, RemoveFieldStatement, RemoveFunctionStatement, + RemoveIndexStatement, RemoveNamespaceStatement, RemoveParamStatement, RemoveStatement, RemoveUserStatement, }, Param, @@ -66,7 +66,7 @@ impl Parser<'_> { if_exists, }) } - t!("TOKEN") => { + t!("ACCESS") => { let if_exists = if self.eat(t!("IF")) { expected!(self, t!("EXISTS")); true @@ -75,28 +75,14 @@ impl Parser<'_> { }; let name = self.next_token_value()?; 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, base, 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") => { let if_exists = if self.eat(t!("IF")) { expected!(self, t!("EXISTS")); diff --git a/core/src/syn/parser/test/stmt.rs b/core/src/syn/parser/test/stmt.rs index d5924112..f573cd1a 100644 --- a/core/src/syn/parser/test/stmt.rs +++ b/core/src/syn/parser/test/stmt.rs @@ -1,5 +1,9 @@ use crate::{ sql::{ + access_type::{ + AccessType, JwtAccess, JwtAccessIssue, JwtAccessVerify, JwtAccessVerifyJwks, + JwtAccessVerifyKey, RecordAccess, + }, block::Entry, changefeed::ChangeFeed, filter::Filter, @@ -8,15 +12,15 @@ use crate::{ statements::{ analyze::AnalyzeStatement, show::ShowSince, show::ShowStatement, sleep::SleepStatement, BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement, - CreateStatement, DefineAnalyzerStatement, DefineDatabaseStatement, - DefineEventStatement, DefineFieldStatement, DefineFunctionStatement, - DefineIndexStatement, DefineNamespaceStatement, DefineParamStatement, DefineStatement, - DefineTableStatement, DefineTokenStatement, DeleteStatement, ForeachStatement, - IfelseStatement, InfoStatement, InsertStatement, KillStatement, OptionStatement, - OutputStatement, RelateStatement, RemoveAnalyzerStatement, RemoveDatabaseStatement, - RemoveEventStatement, RemoveFieldStatement, RemoveFunctionStatement, - RemoveIndexStatement, RemoveNamespaceStatement, RemoveParamStatement, - RemoveScopeStatement, RemoveStatement, RemoveTableStatement, RemoveTokenStatement, + CreateStatement, DefineAccessStatement, DefineAnalyzerStatement, + DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement, + DefineFunctionStatement, DefineIndexStatement, DefineNamespaceStatement, + DefineParamStatement, DefineStatement, DefineTableStatement, DeleteStatement, + ForeachStatement, IfelseStatement, InfoStatement, InsertStatement, KillStatement, + OptionStatement, OutputStatement, RelateStatement, RemoveAccessStatement, + RemoveAnalyzerStatement, RemoveDatabaseStatement, RemoveEventStatement, + RemoveFieldStatement, RemoveFunctionStatement, RemoveIndexStatement, + RemoveNamespaceStatement, RemoveParamStatement, RemoveStatement, RemoveTableStatement, RemoveUserStatement, SelectStatement, SetStatement, ThrowStatement, UpdateStatement, UseStatement, }, @@ -219,26 +223,132 @@ fn parse_define_user() { 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] 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!( parse_stmt, r#"DEFINE TOKEN a ON SCOPE b TYPE EDDSA VALUE "foo" 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::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!( res, - Statement::Define(DefineStatement::Token(DefineTokenStatement { + Statement::Define(DefineStatement::Access(DefineAccessStatement { name: Ident("a".to_string()), - base: Base::Sc(Ident("b".to_string())), - kind: Algorithm::EdDSA, - code: "foo".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, - })) + })), ) } +// 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] fn parse_define_scope() { let res = test_parse!( @@ -247,16 +357,568 @@ fn parse_define_scope() { ) .unwrap(); - // manually compare since DefineScopeStatement creates a random code in its parser. - let Statement::Define(DefineStatement::Scope(stmt)) = res else { + // 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.session, Some(Duration(std::time::Duration::from_secs(1)))); - assert_eq!(stmt.signup, Some(Value::Bool(true))); - assert_eq!(stmt.signin, Some(Value::Bool(false))); + assert_eq!(stmt.if_not_exists, false); + match stmt.kind { + 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] @@ -683,9 +1345,6 @@ fn parse_info() { let res = test_parse!(parse_stmt, "INFO FOR NS").unwrap(); 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(); 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!( res, - Statement::Remove(RemoveStatement::Token(RemoveTokenStatement { - 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 { + Statement::Remove(RemoveStatement::Access(RemoveAccessStatement { name: Ident("foo".to_owned()), + base: Base::Db, if_exists: false, })) ); diff --git a/core/src/syn/parser/test/streaming.rs b/core/src/syn/parser/test/streaming.rs index 0cc36531..a5eec11d 100644 --- a/core/src/syn/parser/test/streaming.rs +++ b/core/src/syn/parser/test/streaming.rs @@ -1,5 +1,6 @@ use crate::{ sql::{ + access_type::{AccessType, JwtAccess, JwtAccessVerify, JwtAccessVerifyKey, RecordAccess}, block::Entry, changefeed::ChangeFeed, filter::Filter, @@ -8,13 +9,13 @@ use crate::{ statements::{ analyze::AnalyzeStatement, show::ShowSince, show::ShowStatement, sleep::SleepStatement, BeginStatement, BreakStatement, CancelStatement, CommitStatement, ContinueStatement, - CreateStatement, DefineAnalyzerStatement, DefineDatabaseStatement, - DefineEventStatement, DefineFieldStatement, DefineFunctionStatement, - DefineIndexStatement, DefineNamespaceStatement, DefineParamStatement, DefineStatement, - DefineTableStatement, DefineTokenStatement, DeleteStatement, ForeachStatement, - IfelseStatement, InfoStatement, InsertStatement, KillStatement, OutputStatement, - RelateStatement, RemoveFieldStatement, RemoveFunctionStatement, RemoveStatement, - SelectStatement, SetStatement, ThrowStatement, UpdateStatement, + CreateStatement, DefineAccessStatement, DefineAnalyzerStatement, + DefineDatabaseStatement, DefineEventStatement, DefineFieldStatement, + DefineFunctionStatement, DefineIndexStatement, DefineNamespaceStatement, + DefineParamStatement, DefineStatement, DefineTableStatement, DeleteStatement, + ForeachStatement, IfelseStatement, InfoStatement, InsertStatement, KillStatement, + OutputStatement, RelateStatement, RemoveFieldStatement, RemoveFunctionStatement, + RemoveStatement, SelectStatement, SetStatement, ThrowStatement, UpdateStatement, }, tokenizer::Tokenizer, 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) { RETURN a } 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 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; @@ -73,7 +74,6 @@ static SOURCE: &str = r#" IF foo { bar } ELSE IF faz { baz } ELSE { baq }; INFO FOR ROOT; INFO FOR NAMESPACE; - INFO FOR SCOPE scope; INFO FOR USER user ON namespace; SELECT bar as foo,[1,2],bar OMIT bar FROM ONLY a,1 WITH INDEX index,index_2 @@ -191,11 +191,21 @@ fn statements() -> Vec { permissions: Permission::Full, if_not_exists: false, })), - Statement::Define(DefineStatement::Token(DefineTokenStatement { + Statement::Define(DefineStatement::Access(DefineAccessStatement { name: Ident("a".to_string()), - base: Base::Sc(Ident("b".to_string())), - kind: Algorithm::EdDSA, - code: "foo".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, })), @@ -435,7 +445,6 @@ fn statements() -> Vec { }), Statement::Info(InfoStatement::Root(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::Select(SelectStatement { expr: Fields( diff --git a/core/src/syn/token/keyword.rs b/core/src/syn/token/keyword.rs index 57ede57f..119949d7 100644 --- a/core/src/syn/token/keyword.rs +++ b/core/src/syn/token/keyword.rs @@ -24,7 +24,9 @@ macro_rules! keyword { } keyword! { + Access => "ACCESS", After => "AFTER", + Algorithm => "ALGORITHM", All => "ALL", Analyze => "ANALYZE", Analyzer => "ANALYZER", @@ -93,6 +95,9 @@ keyword! { Into => "INTO", If => "IF", Is => "IS", + Issuer => "ISSUER", + Jwt => "JWT", + Jwks => "JWKS", Key => "KEY", KeepPrunedConnections => "KEEP_PRUNED_CONNECTIONS", Kill => "KILL", @@ -169,6 +174,7 @@ keyword! { Unset => "UNSET", Update => "UPDATE", Uppercase => "UPPERCASE", + Url => "URL", Use => "USE", User => "USER", Value => "VALUE", diff --git a/core/src/syn/token/mod.rs b/core/src/syn/token/mod.rs index 13973590..431e324a 100644 --- a/core/src/syn/token/mod.rs +++ b/core/src/syn/token/mod.rs @@ -365,8 +365,8 @@ impl TokenKind { ) } - fn algorithm_as_str(algo: Algorithm) -> &'static str { - match algo { + fn algorithm_as_str(alg: Algorithm) -> &'static str { + match alg { Algorithm::EdDSA => "EDDSA", Algorithm::Es256 => "ES256", Algorithm::Es384 => "ES384", @@ -380,7 +380,6 @@ impl TokenKind { Algorithm::Rs256 => "RS256", Algorithm::Rs384 => "RS384", Algorithm::Rs512 => "RS512", - Algorithm::Jwks => "JWKS", } } diff --git a/lib/fuzz/Cargo.lock b/lib/fuzz/Cargo.lock index 7b1c38cb..e75dd337 100644 --- a/lib/fuzz/Cargo.lock +++ b/lib/fuzz/Cargo.lock @@ -227,12 +227,6 @@ dependencies = [ "critical-section", ] -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.2.0" @@ -531,6 +525,33 @@ dependencies = [ "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]] name = "cipher" version = "0.4.4" @@ -732,9 +753,9 @@ dependencies = [ [[package]] name = "echodb" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312221c0bb46e82cd250c818404ef9dce769a4d5a62915c0249b577762eec34a" +checksum = "1ac31e38aeac770dd01b9d6c9ab2a6d7f025815f71105911cf6de073a5db8ee1" dependencies = [ "arc-swap", "imbl", @@ -1063,6 +1084,16 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "hash32" version = "0.2.1" @@ -1622,6 +1653,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "path-clean" version = "1.0.1" @@ -1685,6 +1722,40 @@ dependencies = [ "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]] name = "phf_shared" version = "0.10.0" @@ -1694,6 +1765,16 @@ dependencies = [ "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]] name = "pico-args" version = "0.5.0" @@ -1927,14 +2008,9 @@ dependencies = [ [[package]] name = "reblessive" -version = "0.3.2" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb88832db7cfbac349e0276a84d4fbbcdb9cfe8069affbc765d8fd012265ba45" -dependencies = [ - "atomic-waker", - "pin-project-lite", - "pin-utils", -] +checksum = "4149deda5bd21e0f6ccaa2f907cd542541521dead5861bc51bebdf2af4acaf2a" [[package]] name = "redox_syscall" @@ -2019,9 +2095,9 @@ dependencies = [ [[package]] name = "revision" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87eb86913082f8976b06d07a59f17df9120e6f38b882cf3fc5a45b4499e224b6" +checksum = "588784c1d9453cfd2ce1b7aff06c903513677cf0e63779a0a3085ee8a44f5b17" dependencies = [ "bincode", "chrono", @@ -2037,9 +2113,9 @@ dependencies = [ [[package]] name = "revision-derive" -version = "0.5.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf996fc5f61f1dbec35799b5c00c6dda12e8862e8cb782ed24e10d0292e60ed3" +checksum = "854ff0b6794d4e0aab5e4486870941caefe9f258e63cad2f21b49a6302377c85" dependencies = [ "darling", "proc-macro-error", @@ -2107,6 +2183,27 @@ dependencies = [ "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]] name = "roaring" version = "0.10.3" @@ -2522,7 +2619,7 @@ dependencies = [ "new_debug_unreachable", "once_cell", "parking_lot", - "phf_shared", + "phf_shared 0.10.0", "precomputed-hash", ] @@ -2554,6 +2651,7 @@ dependencies = [ "once_cell", "path-clean", "pharos", + "reblessive", "revision", "ring 0.17.8", "rust_decimal", @@ -2588,6 +2686,7 @@ dependencies = [ "bytes", "cedar-policy", "chrono", + "ciborium", "deunicode", "dmp", "echodb", @@ -2607,6 +2706,7 @@ dependencies = [ "once_cell", "pbkdf2", "pharos", + "phf", "pin-project-lite", "quick_cache", "radix_trie", @@ -2616,6 +2716,7 @@ dependencies = [ "regex-syntax", "revision", "ring 0.17.8", + "rmpv", "roaring", "rust-stemmers", "rust_decimal", @@ -2634,6 +2735,7 @@ dependencies = [ "tracing", "trice", "ulid", + "unicase", "url", "uuid", "wasm-bindgen-futures", @@ -2918,6 +3020,15 @@ dependencies = [ "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]] name = "unicode-bidi" version = "0.3.15" diff --git a/lib/fuzz/fuzz_targets/fuzz_executor.dict b/lib/fuzz/fuzz_targets/fuzz_executor.dict index 2623c42f..570421ad 100644 --- a/lib/fuzz/fuzz_targets/fuzz_executor.dict +++ b/lib/fuzz/fuzz_targets/fuzz_executor.dict @@ -1,4 +1,6 @@ +"ACCESS" "AFTER" +"ALGORITHM" "ALL" "ALLINSIDE" "AND" @@ -61,6 +63,9 @@ "INTERSECTS" "INTO" "IS" +"JWKS" +"JWT" +"KEY" "KILL" "KV" "LET" @@ -89,15 +94,14 @@ "PATH" "PERMISSIONS" "PI" +"RECORD" "RELATE" "REMOVE" "REPLACE" "RETURN" -"SC" "SCHEMAFUL" "SCHEMAFULL" "SCHEMALESS" -"SCOPE" "SELECT" "SESSION" "SET" @@ -116,14 +120,13 @@ "TB" "THEN" "TIMEOUT" -"TK" -"TOKEN" "TRANSACTION" "TRUE" "TYPE" "UNIQUE" "UPDATE" "UPPERCASE" +"URL" "USE" "USER" "VALUE" @@ -277,12 +280,12 @@ "search::offsets(" "session" "session::" +"session::ac(" "session::db(" "session::id(" "session::ip(" "session::ns(" "session::origin(" -"session::sc" # Sleep is just going to slow the fuzzer down # "sleep(" "string" diff --git a/lib/fuzz/fuzz_targets/fuzz_sql_parser.dict b/lib/fuzz/fuzz_targets/fuzz_sql_parser.dict index 8b3a61ed..63e8a6d8 100644 --- a/lib/fuzz/fuzz_targets/fuzz_sql_parser.dict +++ b/lib/fuzz/fuzz_targets/fuzz_sql_parser.dict @@ -1,4 +1,5 @@ "AFTER" +"ALGORITHM" "ALL" "ALLINSIDE" "AND" @@ -61,6 +62,9 @@ "INTERSECTS" "INTO" "IS" +"JWKS" +"JWT" +"KEY" "KILL" "KV" "LET" @@ -89,15 +93,14 @@ "PATH" "PERMISSIONS" "PI" +"RECORD" "RELATE" "REMOVE" "REPLACE" "RETURN" -"SC" "SCHEMAFUL" "SCHEMAFULL" "SCHEMALESS" -"SCOPE" "SELECT" "SESSION" "SET" @@ -116,14 +119,13 @@ "TB" "THEN" "TIMEOUT" -"TK" -"TOKEN" "TRANSACTION" "TRUE" "TYPE" "UNIQUE" "UPDATE" "UPPERCASE" +"URL" "USE" "USER" "VALUE" @@ -277,12 +279,12 @@ "search::offsets(" "session" "session::" +"session::ac(" "session::db(" "session::id(" "session::ip(" "session::ns(" "session::origin(" -"session::sc" "sleep(" "string" "string::concat(" diff --git a/lib/src/api/method/mod.rs b/lib/src/api/method/mod.rs index 4b235337..6665aec8 100644 --- a/lib/src/api/method/mod.rs +++ b/lib/src/api/method/mod.rs @@ -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 /// @@ -401,7 +401,7 @@ where /// use serde::Serialize; /// use surrealdb::sql; /// use surrealdb::opt::auth::Root; - /// use surrealdb::opt::auth::Scope; + /// use surrealdb::opt::auth::Record; /// /// #[derive(Debug, Serialize)] /// struct AuthParams { @@ -423,19 +423,19 @@ where /// // Select the namespace/database to use /// db.use_ns("namespace").use_db("database").await?; /// - /// // Define the scope + /// // Define the user record access /// 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) ) /// SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) ) /// "#; /// db.query(sql).await?.check()?; /// /// // Sign a user up - /// db.signup(Scope { + /// db.signup(Record { /// namespace: "namespace", /// database: "database", - /// scope: "user_scope", + /// access: "user_access", /// params: AuthParams { /// email: "john.doe@example.com".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 /// @@ -530,12 +530,12 @@ where /// # } /// ``` /// - /// Scope signin + /// Record signin /// /// ```no_run /// use serde::Serialize; /// use surrealdb::opt::auth::Root; - /// use surrealdb::opt::auth::Scope; + /// use surrealdb::opt::auth::Record; /// /// #[derive(Debug, Serialize)] /// struct AuthParams { @@ -551,10 +551,10 @@ where /// db.use_ns("namespace").use_db("database").await?; /// /// // Sign a user in - /// db.signin(Scope { + /// db.signin(Record { /// namespace: "namespace", /// database: "database", - /// scope: "user_scope", + /// access: "user_access", /// params: AuthParams { /// email: "john.doe@example.com".into(), /// password: "password123".into(), diff --git a/lib/src/api/method/tests/mod.rs b/lib/src/api/method/tests/mod.rs index dc643676..de6f9099 100644 --- a/lib/src/api/method/tests/mod.rs +++ b/lib/src/api/method/tests/mod.rs @@ -9,8 +9,8 @@ use crate::api::method::tests::types::AuthParams; use crate::api::opt::auth::Database; use crate::api::opt::auth::Jwt; use crate::api::opt::auth::Namespace; +use crate::api::opt::auth::Record; use crate::api::opt::auth::Root; -use crate::api::opt::auth::Scope; use crate::api::opt::PatchOp; use crate::api::Response as QueryResponse; use crate::api::Surreal; @@ -42,10 +42,10 @@ async fn api() { // signup let _: Jwt = DB - .signup(Scope { + .signup(Record { namespace: "test-ns", database: "test-db", - scope: "scope", + access: "access", params: AuthParams {}, }) .await @@ -77,10 +77,10 @@ async fn api() { .await .unwrap(); let _: Jwt = DB - .signin(Scope { + .signin(Record { namespace: "test-ns", database: "test-db", - scope: "scope", + access: "access", params: AuthParams {}, }) .await diff --git a/lib/src/api/opt/auth.rs b/lib/src/api/opt/auth.rs index 570b0932..c3a6e716 100644 --- a/lib/src/api/opt/auth.rs +++ b/lib/src/api/opt/auth.rs @@ -63,24 +63,24 @@ pub struct Database<'a> { impl Credentials for Database<'_> {} -/// Credentials for the scope user +/// Credentials for the record user #[derive(Debug, Serialize)] -pub struct Scope<'a, P> { +pub struct Record<'a, P> { /// The namespace the user has access to #[serde(rename = "ns")] pub namespace: &'a str, /// The database the user has access to #[serde(rename = "db")] pub database: &'a str, - /// The scope to use for signin and signup - #[serde(rename = "sc")] - pub scope: &'a str, + /// The access method to use for signin and signup + #[serde(rename = "ac")] + pub access: &'a str, /// The additional params to use #[serde(flatten)] pub params: P, } -impl Credentials for Scope<'_, P> where P: Serialize {} +impl Credentials for Record<'_, P> where P: Serialize {} /// A JSON Web Token for authenticating with the server. /// diff --git a/lib/tests/api.rs b/lib/tests/api.rs index 2612be71..007410c7 100644 --- a/lib/tests/api.rs +++ b/lib/tests/api.rs @@ -18,8 +18,8 @@ mod api_integration { use surrealdb::opt::auth::Database; use surrealdb::opt::auth::Jwt; use surrealdb::opt::auth::Namespace; + use surrealdb::opt::auth::Record as RecordAccess; use surrealdb::opt::auth::Root; - use surrealdb::opt::auth::Scope; use surrealdb::opt::Config; use surrealdb::opt::PatchOp; use surrealdb::opt::Resource; diff --git a/lib/tests/api/mod.rs b/lib/tests/api/mod.rs index b31bef9e..eabe6257 100644 --- a/lib/tests/api/mod.rs +++ b/lib/tests/api/mod.rs @@ -52,14 +52,14 @@ async fn invalidate() { } #[test_log::test(tokio::test)] -async fn signup_scope() { +async fn signup_record() { let (permit, db) = new_db().await; let database = Ulid::new().to_string(); db.use_ns(NS).use_db(&database).await.unwrap(); - let scope = Ulid::new().to_string(); + let access = Ulid::new().to_string(); 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) ) 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(); drop(permit); response.check().unwrap(); - db.signup(Scope { + db.signup(RecordAccess { namespace: NS, database: &database, - scope: &scope, + access: &access, params: AuthParams { email: "john.doe@example.com", pass: "password123", @@ -121,16 +121,16 @@ async fn signin_db() { } #[test_log::test(tokio::test)] -async fn signin_scope() { +async fn signin_record() { let (permit, db) = new_db().await; let database = Ulid::new().to_string(); db.use_ns(NS).use_db(&database).await.unwrap(); - let scope = Ulid::new().to_string(); - let email = format!("{scope}@example.com"); + let access = Ulid::new().to_string(); + let email = format!("{access}@example.com"); let pass = "password123"; 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) ) 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(); drop(permit); response.check().unwrap(); - db.signup(Scope { + db.signup(RecordAccess { namespace: NS, database: &database, - scope: &scope, + access: &access, params: AuthParams { pass, email: &email, @@ -149,10 +149,10 @@ async fn signin_scope() { }) .await .unwrap(); - db.signin(Scope { + db.signin(RecordAccess { namespace: NS, database: &database, - scope: &scope, + access: &access, params: AuthParams { pass, email: &email, @@ -163,16 +163,16 @@ async fn signin_scope() { } #[test_log::test(tokio::test)] -async fn scope_throws_error() { +async fn record_access_throws_error() { let (permit, db) = new_db().await; let database = Ulid::new().to_string(); db.use_ns(NS).use_db(&database).await.unwrap(); - let scope = Ulid::new().to_string(); - let email = format!("{scope}@example.com"); + let access = Ulid::new().to_string(); + let email = format!("{access}@example.com"); let pass = "password123"; let sql = format!( " - DEFINE SCOPE `{scope}` SESSION 1s + DEFINE ACCESS `{access}` ON DB TYPE RECORD DURATION 1s SIGNUP {{ THROW 'signup_thrown_error' }} SIGNIN {{ THROW 'signin_thrown_error' }} " @@ -182,10 +182,10 @@ async fn scope_throws_error() { response.check().unwrap(); match db - .signup(Scope { + .signup(RecordAccess { namespace: NS, database: &database, - scope: &scope, + access: &access, params: AuthParams { pass, email: &email, @@ -203,10 +203,10 @@ async fn scope_throws_error() { }; match db - .signin(Scope { + .signin(RecordAccess { namespace: NS, database: &database, - scope: &scope, + access: &access, params: AuthParams { pass, email: &email, @@ -225,16 +225,16 @@ async fn scope_throws_error() { } #[test_log::test(tokio::test)] -async fn scope_invalid_query() { +async fn record_access_invalid_query() { let (permit, db) = new_db().await; let database = Ulid::new().to_string(); db.use_ns(NS).use_db(&database).await.unwrap(); - let scope = Ulid::new().to_string(); - let email = format!("{scope}@example.com"); + let access = Ulid::new().to_string(); + let email = format!("{access}@example.com"); let pass = "password123"; let sql = format!( " - DEFINE SCOPE `{scope}` SESSION 1s + DEFINE ACCESS `{access}` ON DB TYPE RECORD DURATION 1s SIGNUP {{ SELECT * FROM ONLY [1, 2] }} SIGNIN {{ SELECT * FROM ONLY [1, 2] }} " @@ -244,10 +244,10 @@ async fn scope_invalid_query() { response.check().unwrap(); match db - .signup(Scope { + .signup(RecordAccess { namespace: NS, database: &database, - scope: &scope, + access: &access, params: AuthParams { pass, email: &email, @@ -255,9 +255,12 @@ async fn scope_invalid_query() { }) .await { - Err(Error::Db(surrealdb::err::Error::SignupQueryFailed)) => (), + Err(Error::Db(surrealdb::err::Error::AccessRecordSignupQueryFailed)) => (), 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!( e, @@ -267,10 +270,10 @@ async fn scope_invalid_query() { }; match db - .signin(Scope { + .signin(RecordAccess { namespace: NS, database: &database, - scope: &scope, + access: &access, params: AuthParams { pass, email: &email, @@ -278,9 +281,12 @@ async fn scope_invalid_query() { }) .await { - Err(Error::Db(surrealdb::err::Error::SigninQueryFailed)) => (), + Err(Error::Db(surrealdb::err::Error::AccessRecordSigninQueryFailed)) => (), 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!( e, diff --git a/lib/tests/create.rs b/lib/tests/create.rs index c0c03bca..4d26193b 100644 --- a/lib/tests/create.rs +++ b/lib/tests/create.rs @@ -236,7 +236,7 @@ async fn create_or_insert_with_permissions() -> Result<(), Error> { CREATE demo SET id = demo:one; 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?; assert_eq!(res.len(), 2); // diff --git a/lib/tests/define.rs b/lib/tests/define.rs index 13f0a08f..767e356e 100644 --- a/lib/tests/define.rs +++ b/lib/tests/define.rs @@ -55,8 +55,8 @@ async fn define_statement_database() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, databases: { test: 'DEFINE DATABASE test' }, - tokens: {}, users: {}, }", ); @@ -84,12 +84,11 @@ async fn define_statement_function() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: { test: 'DEFINE FUNCTION fn::test($first: string, $last: string) { RETURN $first + $last; } PERMISSIONS FULL' }, models: {}, params: {}, - scopes: {}, tables: {}, users: {}, }", @@ -116,12 +115,11 @@ async fn define_statement_table_drop() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: { test: 'DEFINE TABLE test TYPE ANY DROP SCHEMALESS PERMISSIONS NONE' }, users: {}, }", @@ -148,12 +146,11 @@ async fn define_statement_table_schemaless() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMALESS PERMISSIONS NONE' }, users: {}, }", @@ -184,12 +181,11 @@ async fn define_statement_table_schemafull() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE' }, users: {}, }", @@ -216,12 +212,11 @@ async fn define_statement_table_schemaful() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMAFULL PERMISSIONS NONE' }, users: {}, }", @@ -256,12 +251,11 @@ async fn define_statement_table_foreigntable() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: { 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', @@ -289,12 +283,11 @@ async fn define_statement_table_foreigntable() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: { 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 val = Value::parse( r#"{ + accesses: {}, analyzers: { autocomplete: 'DEFINE ANALYZER autocomplete FILTERS LOWERCASE,EDGENGRAM(2,10)', english: 'DEFINE ANALYZER english TOKENIZERS BLANK,CLASS FILTERS LOWERCASE,SNOWBALL(ENGLISH)', htmlAnalyzer: 'DEFINE ANALYZER htmlAnalyzer FUNCTION fn::stripHtml TOKENIZERS BLANK,CLASS' }, - tokens: {}, functions: { stripHtml: "DEFINE FUNCTION fn::stripHtml($html: string) { RETURN string::replace($html, /<[^>]*>/, ''); } PERMISSIONS FULL" }, models: {}, params: {}, - scopes: {}, tables: {}, 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 let check_results = [ - vec!["{ databases: { DB: 'DEFINE DATABASE DB' }, tokens: { }, users: { } }"], - vec!["{ databases: { }, tokens: { }, users: { } }"], + vec!["{ accesses: { }, databases: { DB: 'DEFINE DATABASE DB' }, users: { } }"], + vec!["{ accesses: { }, databases: { }, users: { } }"], ]; 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 let check_results = [ - vec!["{ analyzers: { }, functions: { greet: \"DEFINE FUNCTION fn::greet() { RETURN 'Hello'; } PERMISSIONS FULL\" }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"] + vec!["{ accesses: { }, analyzers: { }, functions: { greet: \"DEFINE FUNCTION fn::greet() { RETURN 'Hello'; } PERMISSIONS FULL\" }, models: { }, params: { }, tables: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"] ]; 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 let check_results = [ - vec!["{ analyzers: { analyzer: 'DEFINE ANALYZER analyzer TOKENIZERS BLANK' }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"] + vec!["{ accesses: { }, analyzers: { analyzer: 'DEFINE ANALYZER analyzer TOKENIZERS BLANK' }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"] ]; let test_cases = [ @@ -1674,17 +1666,17 @@ async fn permissions_checks_define_analyzer() { } #[tokio::test] -async fn permissions_checks_define_token_ns() { +async fn permissions_checks_define_access_ns() { let scenario = HashMap::from([ ("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"), ]); // Define the expected results for the check statement when the test statement succeeded and when it failed let check_results = [ - vec!["{ databases: { }, tokens: { token: \"DEFINE TOKEN token ON NAMESPACE TYPE HS512 VALUE 'secret'\" }, users: { } }"], - vec!["{ databases: { }, tokens: { }, users: { } }"] + vec!["{ accesses: { access: \"DEFINE ACCESS access ON NAMESPACE TYPE JWT ALGORITHM HS512 KEY 'secret' WITH ISSUER KEY 'secret' DURATION 1h\" }, databases: { }, users: { } }"], + vec!["{ accesses: { }, databases: { }, users: { } }"] ]; let test_cases = [ @@ -1716,17 +1708,17 @@ async fn permissions_checks_define_token_ns() { } #[tokio::test] -async fn permissions_checks_define_token_db() { +async fn permissions_checks_define_access_db() { let scenario = HashMap::from([ ("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"), ]); // 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: { token: \"DEFINE TOKEN token ON DATABASE TYPE HS512 VALUE 'secret'\" }, users: { } }"], - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, 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!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"] ]; 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 let check_results = [ - vec!["{ databases: { }, tokens: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\" } }"], - vec!["{ databases: { }, tokens: { }, users: { } }"] + vec!["{ accesses: { }, databases: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\" } }"], + vec!["{ accesses: { }, databases: { }, users: { } }"] ]; 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 let check_results = [ - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, 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: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER\" } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"] ]; let test_cases = [ @@ -1884,28 +1876,28 @@ async fn permissions_checks_define_user_db() { } #[tokio::test] -async fn permissions_checks_define_scope() { +async fn permissions_checks_define_access_record() { let scenario = HashMap::from([ ("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"), ]); // 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: { account: 'DEFINE SCOPE account SESSION 1h' }, tables: { }, tokens: { }, users: { } }"], - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, 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!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"] ]; let test_cases = [ // Root level ((().into(), Role::Owner), ("NS", "DB"), true), - ((().into(), Role::Editor), ("NS", "DB"), true), + ((().into(), Role::Editor), ("NS", "DB"), false), ((().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), ("NS", "DB"), false), ((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false), ((("NS",).into(), Role::Viewer), ("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", "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", "DB"), false), ((("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), @@ -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 let check_results = [ - vec!["{ analyzers: { }, functions: { }, models: { }, params: { param: \"DEFINE PARAM $param VALUE 'foo' PERMISSIONS FULL\" }, scopes: { }, tables: { }, tokens: { }, users: { } }"], - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"] + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { param: \"DEFINE PARAM $param VALUE 'foo' PERMISSIONS FULL\" }, tables: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"] ]; 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 let check_results = [ - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { TB: 'DEFINE TABLE TB TYPE ANY SCHEMALESS PERMISSIONS NONE' }, tokens: { }, users: { } }"], - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"] + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { TB: 'DEFINE TABLE TB TYPE ANY SCHEMALESS PERMISSIONS NONE' }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"] ]; let test_cases = [ @@ -2159,17 +2151,16 @@ async fn define_statement_table_permissions() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: { default: 'DEFINE TABLE default TYPE ANY SCHEMALESS PERMISSIONS NONE', 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' }, - tokens: {}, users: {} }", ); @@ -2499,10 +2490,10 @@ async fn redefining_existing_param_with_if_not_exists_should_error() -> Result<( } #[tokio::test] -async fn redefining_existing_scope_should_not_error() -> Result<(), Error> { +async fn redefining_existing_access_should_not_error() -> Result<(), Error> { let sql = " - DEFINE SCOPE example; - DEFINE SCOPE example; + DEFINE ACCESS example ON DATABASE TYPE JWT ALGORITHM HS512 KEY 'secret'; + DEFINE ACCESS example ON DATABASE TYPE JWT ALGORITHM HS512 KEY 'secret'; "; let dbs = new_ds().await?; 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] -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 = " - DEFINE SCOPE IF NOT EXISTS example; - DEFINE SCOPE IF NOT EXISTS example; + DEFINE ACCESS IF NOT EXISTS example ON DATABASE TYPE JWT ALGORITHM HS512 KEY 'secret'; + DEFINE ACCESS IF NOT EXISTS example ON DATABASE TYPE JWT ALGORITHM HS512 KEY 'secret'; "; let dbs = new_ds().await?; 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); // let tmp = res.remove(0).result.unwrap_err(); - assert!(matches!(tmp, Error::ScAlreadyExists { .. }),); + assert!(matches!(tmp, Error::AccessDbAlreadyExists { .. }),); // Ok(()) } @@ -2578,46 +2569,6 @@ async fn redefining_existing_table_with_if_not_exists_should_error() -> Result<( 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] async fn redefining_existing_user_should_not_error() -> Result<(), Error> { let sql = " @@ -2831,12 +2782,11 @@ async fn define_table_relation_redefinition_info() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: { likes: 'DEFINE TABLE likes TYPE RELATION IN person OUT person SCHEMALESS PERMISSIONS NONE' }, users: {}, }", @@ -2861,12 +2811,11 @@ async fn define_table_relation_redefinition_info() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: { likes: 'DEFINE TABLE likes TYPE RELATION IN person OUT person | thing SCHEMALESS PERMISSIONS NONE' }, users: {}, }", @@ -2891,12 +2840,11 @@ async fn define_table_relation_redefinition_info() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: { likes: 'DEFINE TABLE likes TYPE RELATION IN person OUT person | thing | other SCHEMALESS PERMISSIONS NONE' }, users: {}, }", diff --git a/lib/tests/delete.rs b/lib/tests/delete.rs index 3981f8cf..4b3e880d 100644 --- a/lib/tests/delete.rs +++ b/lib/tests/delete.rs @@ -456,7 +456,7 @@ async fn delete_with_permissions() -> Result<(), Error> { DELETE friends_with:1 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?; assert_eq!(res.len(), 2); // diff --git a/lib/tests/field.rs b/lib/tests/field.rs index 1b070b66..f51b2670 100644 --- a/lib/tests/field.rs +++ b/lib/tests/field.rs @@ -842,7 +842,7 @@ async fn field_definition_edge_permissions() -> Result<(), Error> { RELATE business:one->contact:one->business:two; 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?; assert_eq!(res.len(), 2); // diff --git a/lib/tests/helpers.rs b/lib/tests/helpers.rs index e6785d91..dd138ff7 100644 --- a/lib/tests/helpers.rs +++ b/lib/tests/helpers.rs @@ -24,7 +24,7 @@ pub async fn iam_run_case( sess: &Session, should_succeed: bool, ) -> Result<(), Box> { - // 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(); owner_sess.au = Arc::new(Auth::for_root(Role::Owner)); diff --git a/lib/tests/info.rs b/lib/tests/info.rs index 97747bad..a0832a4b 100644 --- a/lib/tests/info.rs +++ b/lib/tests/info.rs @@ -39,7 +39,7 @@ async fn info_for_ns() { let sql = r#" DEFINE DATABASE DB; 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 "#; let dbs = new_ds().await.unwrap(); @@ -52,7 +52,7 @@ async fn info_for_ns() { assert!(out.is_ok(), "Unexpected error: {:?}", out); let output_regex = Regex::new( - r"\{ databases: \{ DB: .* \}, tokens: \{ token: .* \}, users: \{ user: .* \} \}", + r"\{ accesses: \{ access: .* \}, databases: \{ DB: .* \}, users: \{ user: .* \} \}", ) .unwrap(); let out_str = out.unwrap().to_string(); @@ -68,9 +68,9 @@ async fn info_for_ns() { async fn info_for_db() { let sql = r#" 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 TOKEN token ON DB TYPE HS512 VALUE 'secret'; DEFINE FUNCTION fn::greet() {RETURN "Hello";}; DEFINE PARAM $param VALUE "foo"; DEFINE ANALYZER analyzer TOKENIZERS BLANK; @@ -85,33 +85,7 @@ async fn info_for_db() { let out = res.pop().unwrap().output(); 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 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 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), @@ -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 let check_results = [ - vec!["{ databases: { }, tokens: { }, users: { } }"], - vec!["{ databases: { }, tokens: { }, users: { } }"], + vec!["{ accesses: { }, databases: { }, users: { } }"], + vec!["{ accesses: { }, databases: { }, users: { } }"], ]; 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 let check_results = [ - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"], ]; let test_cases = [ @@ -344,45 +318,6 @@ async fn permissions_checks_info_db() { 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] async fn permissions_checks_info_table() { let scenario = HashMap::from([ diff --git a/lib/tests/param.rs b/lib/tests/param.rs index 510b088c..fe9a39da 100644 --- a/lib/tests/param.rs +++ b/lib/tests/param.rs @@ -26,12 +26,11 @@ async fn define_global_param() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: { test: 'DEFINE PARAM $test VALUE 12345 PERMISSIONS FULL' }, - scopes: {}, tables: {}, users: {}, }", diff --git a/lib/tests/remove.rs b/lib/tests/remove.rs index 5c3ac1ec..87418248 100644 --- a/lib/tests/remove.rs +++ b/lib/tests/remove.rs @@ -36,12 +36,11 @@ async fn remove_statement_table() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: {}, users: {} }", @@ -71,12 +70,11 @@ async fn remove_statement_analyzer() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: {}, users: {} }", @@ -418,9 +416,9 @@ async fn should_not_error_when_remove_param_if_exists() -> Result<(), Error> { } #[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 = " - REMOVE SCOPE foo; + REMOVE ACCESS foo ON DB; "; let dbs = new_ds().await?; 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); // let tmp = res.remove(0).result.unwrap_err(); - assert!(matches!(tmp, Error::ScNotFound { .. }),); + assert!(matches!(tmp, Error::DaNotFound { .. }),); Ok(()) } #[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 = " - REMOVE SCOPE IF EXISTS foo; - "; - 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; + REMOVE ACCESS IF EXISTS foo ON DB; "; let dbs = new_ds().await?; 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 let check_results = [ - vec!["{ databases: { }, tokens: { }, users: { } }"], - vec!["{ databases: { DB: 'DEFINE DATABASE DB' }, tokens: { }, users: { } }"], + vec!["{ accesses: { }, databases: { }, users: { } }"], + vec!["{ accesses: { }, databases: { DB: 'DEFINE DATABASE DB' }, users: { } }"], ]; 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 let check_results = [ - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], - vec!["{ analyzers: { }, functions: { greet: \"DEFINE FUNCTION fn::greet() { RETURN 'Hello'; } PERMISSIONS FULL\" }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { greet: \"DEFINE FUNCTION fn::greet() { RETURN 'Hello'; } PERMISSIONS FULL\" }, models: { }, params: { }, tables: { }, users: { } }"], ]; 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 let check_results = [ - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], - vec!["{ analyzers: { analyzer: 'DEFINE ANALYZER analyzer TOKENIZERS BLANK' }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { analyzer: 'DEFINE ANALYZER analyzer TOKENIZERS BLANK' }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"], ]; let test_cases = [ @@ -686,17 +652,17 @@ async fn permissions_checks_remove_analyzer() { } #[tokio::test] -async fn permissions_checks_remove_ns_token() { +async fn permissions_checks_remove_ns_access() { let scenario = HashMap::from([ - ("prepare", "DEFINE TOKEN token ON NS TYPE HS512 VALUE 'secret'"), - ("test", "REMOVE TOKEN token ON NS"), + ("prepare", "DEFINE ACCESS access ON NS TYPE JWT ALGORITHM HS512 KEY 'secret'"), + ("test", "REMOVE ACCESS access ON NS"), ("check", "INFO FOR NS"), ]); // Define the expected results for the check statement when the test statement succeeded and when it failed let check_results = [ - vec!["{ databases: { }, tokens: { }, users: { } }"], - vec!["{ databases: { }, tokens: { token: \"DEFINE TOKEN token ON NAMESPACE TYPE HS512 VALUE 'secret'\" }, users: { } }"], + vec!["{ accesses: { }, databases: { }, 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 = [ @@ -728,17 +694,17 @@ async fn permissions_checks_remove_ns_token() { } #[tokio::test] -async fn permissions_checks_remove_db_token() { +async fn permissions_checks_remove_db_access() { let scenario = HashMap::from([ - ("prepare", "DEFINE TOKEN token ON DB TYPE HS512 VALUE 'secret'"), - ("test", "REMOVE TOKEN token ON DB"), + ("prepare", "DEFINE ACCESS access ON DB TYPE JWT ALGORITHM HS512 KEY 'secret'"), + ("test", "REMOVE ACCESS access ON DB"), ("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: { }, tables: { }, tokens: { token: \"DEFINE TOKEN token ON DATABASE TYPE HS512 VALUE 'secret'\" }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, 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 = [ @@ -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 let check_results = [ - vec!["{ databases: { }, tokens: { }, users: { } }"], - vec!["{ databases: { }, tokens: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\" } }"], + vec!["{ accesses: { }, databases: { }, users: { } }"], + vec!["{ accesses: { }, databases: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\" } }"], ]; 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 let check_results = [ - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, 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: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER\" } }"], ]; let test_cases = [ @@ -895,48 +861,6 @@ async fn permissions_checks_remove_db_user() { 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] async fn permissions_checks_remove_param() { 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 let check_results = [ - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, users: { } }"], - vec!["{ analyzers: { }, functions: { }, models: { }, params: { param: \"DEFINE PARAM $param VALUE 'foo' PERMISSIONS FULL\" }, scopes: { }, tables: { }, tokens: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { param: \"DEFINE PARAM $param VALUE 'foo' PERMISSIONS FULL\" }, tables: { }, users: { } }"], ]; 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 let check_results = [ - vec!["{ analyzers: { }, functions: { }, models: { }, params: { }, scopes: { }, tables: { }, tokens: { }, 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: { }, users: { } }"], + vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { TB: 'DEFINE TABLE TB TYPE ANY SCHEMALESS PERMISSIONS NONE' }, users: { } }"], ]; let test_cases = [ diff --git a/lib/tests/strict.rs b/lib/tests/strict.rs index 2be73b05..2b52130e 100644 --- a/lib/tests/strict.rs +++ b/lib/tests/strict.rs @@ -242,8 +242,8 @@ async fn loose_mode_all_ok() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, databases: { test: 'DEFINE DATABASE test' }, - tokens: {}, users: {}, }", ); @@ -252,12 +252,11 @@ async fn loose_mode_all_ok() -> Result<(), Error> { let tmp = res.remove(0).result?; let val = Value::parse( "{ + accesses: {}, analyzers: {}, - tokens: {}, functions: {}, models: {}, params: {}, - scopes: {}, tables: { test: 'DEFINE TABLE test TYPE ANY SCHEMALESS PERMISSIONS NONE' }, users: {}, }", diff --git a/tests/common/socket.rs b/tests/common/socket.rs index 57d5f0c4..ddb69ba8 100644 --- a/tests/common/socket.rs +++ b/tests/common/socket.rs @@ -41,7 +41,7 @@ struct SigninParams<'a> { #[serde(skip_serializing_if = "Option::is_none")] db: Option<&'a str>, #[serde(skip_serializing_if = "Option::is_none")] - sc: Option<&'a str>, + ac: Option<&'a str>, } enum SocketMsg { @@ -385,7 +385,7 @@ impl Socket { pass: &str, ns: Option<&str>, db: Option<&str>, - sc: Option<&str>, + ac: Option<&str>, ) -> Result { // Send message and receive response let msg = self @@ -396,7 +396,7 @@ impl Socket { pass, ns, db, - sc + ac }]), ) .await?; diff --git a/tests/common/tests.rs b/tests/common/tests.rs index d321de19..2deb13c5 100644 --- a/tests/common/tests.rs +++ b/tests/common/tests.rs @@ -35,11 +35,11 @@ async fn info() -> Result<(), Box> { socket.send_message_use(Some(NS), Some(DB)).await?; // Define a user table socket.send_message_query("DEFINE TABLE user PERMISSIONS FULL").await?; - // Define a user scope + // Define a user record access method socket .send_message_query( 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) ) SIGNIN ( SELECT * FROM user WHERE user = $user AND crypto::argon2::compare(pass, $pass) ) ; @@ -57,8 +57,8 @@ async fn info() -> Result<(), Box> { "#, ) .await?; - // Sign in as scope user - socket.send_message_signin("user", "pass", Some(NS), Some(DB), Some("scope")).await?; + // Sign in as record user + socket.send_message_signin("user", "pass", Some(NS), Some(DB), Some("user")).await?; // Send INFO command let res = socket.send_request("info", json!([])).await?; assert!(res["result"].is_object(), "result: {:?}", res); @@ -79,11 +79,11 @@ async fn signup() -> Result<(), Box> { socket.send_message_signin(USER, PASS, None, None, None).await?; // Specify a namespace and database socket.send_message_use(Some(NS), Some(DB)).await?; - // Setup the scope + // Define a user record access method socket .send_message_query( 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) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) ;"#, @@ -96,7 +96,7 @@ async fn signup() -> Result<(), Box> { json!([{ "ns": NS, "db": DB, - "sc": "scope", + "ac": "user", "email": "email@email.com", "pass": "pass", }]), @@ -127,11 +127,11 @@ async fn signin() -> Result<(), Box> { socket.send_message_signin(USER, PASS, None, None, None).await?; // Specify a namespace and database socket.send_message_use(Some(NS), Some(DB)).await?; - // Setup the scope + // Define a user record access method socket .send_message_query( 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) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) ;"#, @@ -145,7 +145,7 @@ async fn signin() -> Result<(), Box> { [{ "ns": NS, "db": DB, - "sc": "scope", + "ac": "user", "email": "email@email.com", "pass": "pass", }] @@ -170,7 +170,7 @@ async fn signin() -> Result<(), Box> { [{ "ns": NS, "db": DB, - "sc": "scope", + "ac": "user", "email": "email@email.com", "pass": "pass", }]), @@ -868,11 +868,11 @@ async fn variable_auth_live_query() -> Result<(), Box> { socket_permanent.send_message_signin(USER, PASS, None, None, None).await?; // Specify a namespace and database socket_permanent.send_message_use(Some(NS), Some(DB)).await?; - // Setup the scope + // Define a user record access method socket_permanent .send_message_query( 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) ) 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> { json!([{ "ns": NS, "db": DB, - "sc": "scope", + "ac": "user", "email": "email@email.com", "pass": "pass", }]), @@ -933,23 +933,23 @@ async fn session_expiration() { socket.send_message_signin(USER, PASS, None, None, None).await.unwrap(); // Specify a namespace and database socket.send_message_use(Some(NS), Some(DB)).await.unwrap(); - // Setup the scope + // Define a user record access method socket .send_message_query( 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) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) ;"#, ) .await .unwrap(); - // Create resource that requires a scope session to query + // Create resource that requires a session with the access method to query socket .send_message_query( r#" DEFINE TABLE test SCHEMALESS - PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope" + PERMISSIONS FOR select, create, update, delete WHERE $access = "user" ;"#, ) .await @@ -970,7 +970,7 @@ async fn session_expiration() { [{ "ns": NS, "db": DB, - "sc": "scope", + "ac": "user", "email": "email@email.com", "pass": "pass", }] @@ -1012,7 +1012,7 @@ async fn session_expiration() { [{ "ns": NS, "db": DB, - "sc": "scope", + "ac": "user", "email": "email@email.com", "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(); // Specify a namespace and database socket.send_message_use(Some(NS), Some(DB)).await.unwrap(); - // Setup the scope + // Define a user record access method socket .send_message_query( 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) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) ;"#, ) .await .unwrap(); - // Create resource that requires a scope session to query + // Create resource that requires a session with the access method to query socket .send_message_query( r#" DEFINE TABLE test SCHEMALESS - PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope" + PERMISSIONS FOR select, create, update, delete WHERE $access = "user" ;"#, ) .await @@ -1080,7 +1080,7 @@ async fn session_expiration_operations() { [{ "ns": NS, "db": DB, - "sc": "scope", + "ac": "user", "email": "email@email.com", "pass": "pass", }] @@ -1217,7 +1217,7 @@ async fn session_expiration_operations() { json!([{ "ns": NS, "db": DB, - "sc": "scope", + "ac": "user", "email": "another@email.com", "pass": "pass", }]), @@ -1248,7 +1248,7 @@ async fn session_expiration_operations() { [{ "ns": NS, "db": DB, - "sc": "scope", + "ac": "user", "email": "another@email.com", "pass": "pass", }] @@ -1299,23 +1299,23 @@ async fn session_reauthentication() { socket.send_message_query("INFO FOR ROOT").await.unwrap(); // Specify a namespace and database socket.send_message_use(Some(NS), Some(DB)).await.unwrap(); - // Setup the scope + // Define a user record access method socket .send_message_query( 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) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) ;"#, ) .await .unwrap(); - // Create resource that requires a scope session to query + // Create resource that requires a session with the access method to query socket .send_message_query( r#" DEFINE TABLE test SCHEMALESS - PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope" + PERMISSIONS FOR select, create, update, delete WHERE $access = "user" ;"#, ) .await @@ -1336,7 +1336,7 @@ async fn session_reauthentication() { [{ "ns": NS, "db": DB, - "sc": "scope", + "ac": "user", "email": "email@email.com", "pass": "pass", }] @@ -1353,7 +1353,7 @@ async fn session_reauthentication() { assert!(res["result"].is_string(), "result: {:?}", res); let res = res["result"].as_str().unwrap(); assert!(res.starts_with("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9"), "result: {}", res); - // Authenticate using the scope token + // Authenticate using the token socket.send_request("authenticate", json!([res,])).await.unwrap(); // Check that we do not have root access let res = socket.send_message_query("INFO FOR ROOT").await.unwrap(); @@ -1363,7 +1363,7 @@ async fn session_reauthentication() { "result: {:?}", 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(); assert_eq!(res[0]["result"], json!(["yes"]), "result: {:?}", res); // Authenticate using the root token @@ -1387,23 +1387,23 @@ async fn session_reauthentication_expired() { socket.send_message_query("INFO FOR ROOT").await.unwrap(); // Specify a namespace and database socket.send_message_use(Some(NS), Some(DB)).await.unwrap(); - // Setup the scope + // Define a user record access method socket .send_message_query( 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) ) SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(pass, $pass) ) ;"#, ) .await .unwrap(); - // Create resource that requires a scope session to query + // Create resource that requires a session with the access method to query socket .send_message_query( r#" DEFINE TABLE test SCHEMALESS - PERMISSIONS FOR select, create, update, delete WHERE $scope = "scope" + PERMISSIONS FOR select, create, update, delete WHERE $access = "user" ;"#, ) .await @@ -1424,7 +1424,7 @@ async fn session_reauthentication_expired() { [{ "ns": NS, "db": DB, - "sc": "scope", + "ac": "user", "email": "email@email.com", "pass": "pass", }] @@ -1441,7 +1441,7 @@ async fn session_reauthentication_expired() { assert!(res["result"].is_string(), "result: {:?}", res); let res = res["result"].as_str().unwrap(); 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(); // Wait two seconds for token to expire tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; diff --git a/tests/http_integration.rs b/tests/http_integration.rs index df933636..28ef9800 100644 --- a/tests/http_integration.rs +++ b/tests/http_integration.rs @@ -1160,14 +1160,14 @@ mod http_integration { .default_headers(headers) .build()?; - // Create a scope + // Define a record access method { let res = client .post(format!("http://{addr}/sql")) .basic_auth(USER, Some(PASS)) .body( 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) ) 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?); } - // Signup into the scope + // Signup using the defined record access method { let req_body = serde_json::to_string( json!({ "ns": ns, "db": db, - "sc": "scope", + "ac": "user", "email": "email@email.com", "pass": "pass", })