From e9ef8855cffb089b38e213c9162585c0001a6ab3 Mon Sep 17 00:00:00 2001 From: Tobie Morgan Hitchcock Date: Sat, 19 Aug 2023 23:19:16 +0100 Subject: [PATCH] Ensure permissions are enforced on edge `in` and `out` fields (#2465) --- lib/src/dbs/session.rs | 29 +++++++- lib/src/doc/relate.rs | 89 +++++++++++++++++-------- lib/src/iam/auth.rs | 4 ++ lib/src/iam/entities/resources/level.rs | 12 ++-- lib/src/iam/signin.rs | 5 +- lib/tests/field.rs | 83 +++++++++++++++++++++++ 6 files changed, 186 insertions(+), 36 deletions(-) diff --git a/lib/src/dbs/session.rs b/lib/src/dbs/session.rs index f6bae6b5..54e082e0 100644 --- a/lib/src/dbs/session.rs +++ b/lib/src/dbs/session.rs @@ -35,12 +35,19 @@ impl Session { self.ns = Some(ns.to_owned()); self } + /// Set the selected database for the session pub fn with_db(mut self, db: &str) -> Session { self.db = Some(db.to_owned()); self } + /// Set the selected database for the session + pub fn with_sc(mut self, sc: &str) -> Session { + self.sc = Some(sc.to_owned()); + self + } + // Set the realtime functionality of the session pub fn with_rt(mut self, rt: bool) -> Session { self.rt = rt; @@ -51,14 +58,17 @@ impl Session { pub(crate) fn ns(&self) -> Option> { self.ns.as_deref().map(Into::into) } + /// Retrieves the selected database pub(crate) fn db(&self) -> Option> { self.db.as_deref().map(Into::into) } + /// Checks if live queries are allowed pub(crate) fn live(&self) -> bool { self.rt } + /// Convert a session into a runtime pub(crate) fn context<'a>(&self, mut ctx: Context<'a>) -> Context<'a> { // Add scope auth data @@ -88,8 +98,9 @@ impl Session { /// Create a system session for a given level and role pub fn for_level(level: Level, role: Role) -> Session { + // Create a new session let mut sess = Session::default(); - + // Set the session details match level { Level::Root => { sess.au = Arc::new(Auth::for_root(role)); @@ -108,6 +119,22 @@ 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 { + Session { + au: Arc::new(Auth::for_sc(rid.to_string(), ns, db, sc)), + 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), + } + } + /// Create a system session for the root level with Owner role pub fn owner() -> Session { Session::for_level(Level::Root, Role::Owner) diff --git a/lib/src/doc/relate.rs b/lib/src/doc/relate.rs index 0f8d3559..23d769ae 100644 --- a/lib/src/doc/relate.rs +++ b/lib/src/doc/relate.rs @@ -13,31 +13,68 @@ impl<'a> Document<'a> { txn: &Transaction, stm: &Statement<'_>, ) -> Result { - // Check if allowed - self.allow(ctx, opt, txn, stm).await?; - // Alter record data - self.alter(ctx, opt, txn, stm).await?; - // Merge fields data - self.field(ctx, opt, txn, stm).await?; - // Reset fields data - self.reset(ctx, opt, txn, stm).await?; - // Clean fields data - self.clean(ctx, opt, txn, stm).await?; - // Check if allowed - self.allow(ctx, opt, txn, stm).await?; - // Store record edges - self.edges(ctx, opt, txn, stm).await?; - // Store index data - self.index(ctx, opt, txn, stm).await?; - // Store record data - self.store(ctx, opt, txn, stm).await?; - // Run table queries - self.table(ctx, opt, txn, stm).await?; - // Run lives queries - self.lives(ctx, opt, txn, stm).await?; - // Run event queries - self.event(ctx, opt, txn, stm).await?; - // Yield document - self.pluck(ctx, opt, txn, stm).await + // Check current record + match self.current.doc.is_some() { + // Create new edge + false => { + // Store record edges + self.edges(ctx, opt, txn, stm).await?; + // Alter record data + self.alter(ctx, opt, txn, stm).await?; + // Merge fields data + self.field(ctx, opt, txn, stm).await?; + // Reset fields data + self.reset(ctx, opt, txn, stm).await?; + // Clean fields data + self.clean(ctx, opt, txn, stm).await?; + // Check if allowed + self.allow(ctx, opt, txn, stm).await?; + // Store index data + self.index(ctx, opt, txn, stm).await?; + // Store record data + self.store(ctx, opt, txn, stm).await?; + // Run table queries + self.table(ctx, opt, txn, stm).await?; + // Run lives queries + self.lives(ctx, opt, txn, stm).await?; + // Run change feeds queries + self.changefeeds(ctx, opt, txn, stm).await?; + // Run event queries + self.event(ctx, opt, txn, stm).await?; + // Yield document + self.pluck(ctx, opt, txn, stm).await + } + // Update old edge + true => { + // Check if allowed + self.allow(ctx, opt, txn, stm).await?; + // Store record edges + self.edges(ctx, opt, txn, stm).await?; + // Alter record data + self.alter(ctx, opt, txn, stm).await?; + // Merge fields data + self.field(ctx, opt, txn, stm).await?; + // Reset fields data + self.reset(ctx, opt, txn, stm).await?; + // Clean fields data + self.clean(ctx, opt, txn, stm).await?; + // Check if allowed + self.allow(ctx, opt, txn, stm).await?; + // Store index data + self.index(ctx, opt, txn, stm).await?; + // Store record data + self.store(ctx, opt, txn, stm).await?; + // Run table queries + self.table(ctx, opt, txn, stm).await?; + // Run lives queries + self.lives(ctx, opt, txn, stm).await?; + // Run change feeds queries + self.changefeeds(ctx, opt, txn, stm).await?; + // Run event queries + self.event(ctx, opt, txn, stm).await?; + // Yield document + self.pluck(ctx, opt, txn, stm).await + } + } } } diff --git a/lib/src/iam/auth.rs b/lib/src/iam/auth.rs index c78a35b3..59256c2a 100644 --- a/lib/src/iam/auth.rs +++ b/lib/src/iam/auth.rs @@ -66,6 +66,10 @@ 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())) + } + // // Permission checks // diff --git a/lib/src/iam/entities/resources/level.rs b/lib/src/iam/entities/resources/level.rs index 1ae71d55..6032a953 100644 --- a/lib/src/iam/entities/resources/level.rs +++ b/lib/src/iam/entities/resources/level.rs @@ -123,20 +123,20 @@ impl From<()> for Level { } impl From<(&str,)> for Level { - fn from(val: (&str,)) -> Self { - Level::Namespace(val.0.to_owned()) + fn from((ns,): (&str,)) -> Self { + Level::Namespace(ns.to_owned()) } } impl From<(&str, &str)> for Level { - fn from(val: (&str, &str)) -> Self { - Level::Database(val.0.to_owned(), val.1.to_owned()) + fn from((ns, db): (&str, &str)) -> Self { + Level::Database(ns.to_owned(), db.to_owned()) } } impl From<(&str, &str, &str)> for Level { - fn from(val: (&str, &str, &str)) -> Self { - Level::Scope(val.0.to_owned(), val.1.to_owned(), val.2.to_owned()) + fn from((ns, db, sc): (&str, &str, &str)) -> Self { + Level::Scope(ns.to_owned(), db.to_owned(), sc.to_owned()) } } diff --git a/lib/src/iam/signin.rs b/lib/src/iam/signin.rs index 1c684788..885bd659 100644 --- a/lib/src/iam/signin.rs +++ b/lib/src/iam/signin.rs @@ -1,3 +1,5 @@ +use super::verify::verify_creds; +use super::{Actor, Level}; use crate::cnf::SERVER_NAME; use crate::dbs::Session; use crate::err::Error; @@ -10,9 +12,6 @@ use chrono::{Duration, Utc}; use jsonwebtoken::{encode, EncodingKey}; use std::sync::Arc; -use super::verify::verify_creds; -use super::{Actor, Level}; - pub async fn signin( kvs: &Datastore, session: &mut Session, diff --git a/lib/tests/field.rs b/lib/tests/field.rs index c03ec3ea..71890972 100644 --- a/lib/tests/field.rs +++ b/lib/tests/field.rs @@ -3,6 +3,7 @@ use parse::Parse; use surrealdb::dbs::Session; use surrealdb::err::Error; use surrealdb::kvs::Datastore; +use surrealdb::sql::Thing; use surrealdb::sql::Value; #[tokio::test] @@ -507,3 +508,85 @@ async fn field_definition_value_reference_with_future() -> Result<(), Error> { // Ok(()) } + +#[tokio::test] +async fn field_definition_edge_permissions() -> Result<(), Error> { + let sql = " + DEFINE TABLE user SCHEMAFULL; + DEFINE TABLE business SCHEMAFULL; + DEFINE FIELD owner ON TABLE business TYPE record; + DEFINE TABLE contact SCHEMAFULL PERMISSIONS FOR create WHERE in.owner.id = $auth.id; + INSERT INTO user (id, name) VALUES (user:one, 'John'), (user:two, 'Lucy'); + INSERT INTO business (id, owner) VALUES (business:one, user:one), (business:two, user:two); + "; + let dbs = Datastore::new("memory").await?.with_auth_enabled(true); + let ses = Session::owner().with_ns("test").with_db("test"); + let res = &mut dbs.execute(sql, &ses, None).await?; + assert_eq!(res.len(), 6); + // + let tmp = res.remove(0).result; + assert!(tmp.is_ok()); + // + let tmp = res.remove(0).result; + assert!(tmp.is_ok()); + // + let tmp = res.remove(0).result; + assert!(tmp.is_ok()); + // + let tmp = res.remove(0).result; + assert!(tmp.is_ok()); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + id: user:one, + }, + { + id: user:two, + }, + ]", + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + id: business:one, + owner: user:one, + }, + { + id: business:two, + owner: user:two, + }, + ]", + ); + assert_eq!(tmp, val); + // + let sql = " + 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 res = &mut dbs.execute(sql, &ses, None).await?; + assert_eq!(res.len(), 2); + // + let tmp = res.remove(0).result?; + let val = Value::parse( + "[ + { + id: contact:one, + in: business:one, + out: business:two, + }, + ]", + ); + assert_eq!(tmp, val); + // + let tmp = res.remove(0).result?; + let val = Value::parse("[]"); + assert_eq!(tmp, val); + // + Ok(()) +}