surrealdb-core 3.2.0

A scalable, distributed, collaborative, document-graph database, for the realtime web
Documentation
use anyhow::Result;
use revision::revisioned;
use serde::{Deserialize, Serialize};

use super::{Action, Actor, Level, Resource, Role, is_allowed};
use crate::iam::AuthLimit;

/// Specifies the current authentication for the datastore execution context.
#[revisioned(revision = 1)]
#[derive(Clone, Default, Debug, Eq, PartialEq, PartialOrd, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub struct Auth {
	actor: Actor,
}

impl Auth {
	pub fn new(actor: Actor) -> Self {
		Self {
			actor,
		}
	}

	pub fn id(&self) -> &str {
		self.actor.id()
	}

	/// Return current authentication level
	pub fn level(&self) -> &Level {
		self.actor.level()
	}

	/// Check if the current auth is anonymous
	pub fn is_anon(&self) -> bool {
		matches!(self.level(), Level::No)
	}

	/// Check if the current level is Root
	pub fn is_root(&self) -> bool {
		matches!(self.level(), Level::Root)
	}

	/// Check if the current level is Namespace
	pub fn is_ns(&self) -> bool {
		matches!(self.level(), Level::Namespace(_))
	}

	/// Check if the current level is Database
	pub fn is_db(&self) -> bool {
		matches!(self.level(), Level::Database(_, _))
	}

	/// Check if the current level is Record
	pub fn is_record(&self) -> bool {
		matches!(self.level(), Level::Record(_, _, _))
	}

	/// Check if the current level is Namespace, and the namespace matches
	pub fn is_ns_check(&self, ns: &str) -> bool {
		matches!(self.level(), Level::Namespace(n) if n.eq(ns))
	}

	/// Check if the current level is Database, and the namespace and database
	/// match
	pub fn is_db_check(&self, ns: &str, db: &str) -> bool {
		matches!(self.level(), Level::Database(n, d) if n.eq(ns) && d.eq(db))
	}

	/// Check whether the authenticated level is permitted to operate within the
	/// given namespace and database.
	///
	/// This is the tenant-boundary gate for entry points that derive the target
	/// namespace/database from caller-controlled input — most notably the custom
	/// API HTTP route `/api/:ns/:db/:endpoint`, whose handlers run with
	/// permissions disabled. The authenticated [`Level`] is the source of truth;
	/// the session's *selected* `ns`/`db` are not, because they are overwritten
	/// from the request before dispatch.
	///
	/// - Root principals may act in any namespace/database.
	/// - Namespace principals are confined to their own namespace (any database).
	/// - Database and record principals are confined to their exact namespace/database.
	/// - Anonymous (unauthenticated) sessions carry no tenant identity, so they are left to the
	///   endpoint's own permission checks.
	///
	/// This is purely a namespace/database scope check; it does not replace
	/// role or `PERMISSIONS` evaluation.
	pub fn can_access_ns_db(&self, ns: &str, db: &str) -> bool {
		match self.level() {
			Level::Root => true,
			Level::Namespace(n) => n.eq(ns),
			Level::Database(n, d) => n.eq(ns) && d.eq(db),
			Level::Record(n, d, _) => n.eq(ns) && d.eq(db),
			Level::No => true,
		}
	}

	/// System Auth helpers
	///
	/// These are not stored in the database and are used for internal
	/// operations Do not use for authentication
	pub fn for_root(role: Role) -> Self {
		Self::new(Actor::new("system_auth".into(), vec![role], Level::Root))
	}

	pub fn for_ns(role: Role, ns: &str) -> Self {
		Self::new(Actor::new("system_auth".into(), vec![role], Level::Namespace(ns.to_owned())))
	}

	pub fn for_db(role: Role, ns: &str, db: &str) -> Self {
		Self::new(Actor::new(
			"system_auth".into(),
			vec![role],
			Level::Database(ns.to_owned(), db.to_owned()),
		))
	}

	pub fn for_record(rid: String, ns: &str, db: &str, ac: &str) -> Self {
		Self::new(Actor::new(
			rid,
			vec![],
			Level::Record(ns.to_owned(), db.to_owned(), ac.to_owned()),
		))
	}

	pub fn new_limited(&self, limit: &AuthLimit) -> Self {
		Self::new(self.actor.new_limited(limit))
	}

	pub fn max_role(&self) -> Option<Role> {
		self.actor.max_role()
	}

	//
	// Permission checks
	//

	/// Checks if the current auth is allowed to perform an action on a given
	/// resource
	pub fn is_allowed(&self, action: Action, res: &Resource) -> Result<()> {
		is_allowed(&self.actor, &action, res)
			.map_err(crate::err::Error::from)
			.map_err(anyhow::Error::new)
	}

	/// Checks if the current actor has a given role
	pub fn has_role(&self, role: Role) -> bool {
		self.actor.has_role(role)
	}

	/// Checks if the current actor has a Owner role
	pub fn has_owner_role(&self) -> bool {
		self.actor.has_owner_role()
	}

	/// Checks if the current actor has a Editor role
	pub fn has_editor_role(&self) -> bool {
		self.actor.has_editor_role()
	}

	/// Checks if the current actor has a Viewer role
	pub fn has_viewer_role(&self) -> bool {
		self.actor.has_viewer_role()
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn can_access_ns_db_enforces_tenant_boundary() {
		// Root may access any namespace/database.
		let root = Auth::for_root(Role::Viewer);
		assert!(root.can_access_ns_db("a", "x"));
		assert!(root.can_access_ns_db("b", "y"));

		// Namespace principals are confined to their namespace (any database).
		let ns = Auth::for_ns(Role::Viewer, "a");
		assert!(ns.can_access_ns_db("a", "x"));
		assert!(ns.can_access_ns_db("a", "y"));
		assert!(!ns.can_access_ns_db("b", "x"));

		// Database principals are confined to their exact namespace/database.
		let db = Auth::for_db(Role::Viewer, "a", "x");
		assert!(db.can_access_ns_db("a", "x"));
		assert!(!db.can_access_ns_db("a", "y"));
		assert!(!db.can_access_ns_db("b", "x"));

		// Record principals are confined to their namespace/database.
		let rec = Auth::for_record("user:1".to_string(), "a", "x", "ac");
		assert!(rec.can_access_ns_db("a", "x"));
		assert!(!rec.can_access_ns_db("a", "y"));
		assert!(!rec.can_access_ns_db("b", "x"));

		// Anonymous sessions carry no tenant identity; the scope test is
		// permissive and the endpoint's own permission checks remain the gate.
		assert!(Auth::default().can_access_ns_db("a", "x"));
	}
}