Skip to main content

surrealdb_core/iam/
auth.rs

1use anyhow::Result;
2use revision::revisioned;
3use serde::{Deserialize, Serialize};
4
5use super::{Action, Actor, Level, Resource, Role, is_allowed};
6use crate::iam::AuthLimit;
7
8/// Specifies the current authentication for the datastore execution context.
9#[revisioned(revision = 1)]
10#[derive(Clone, Default, Debug, Eq, PartialEq, PartialOrd, Hash, Serialize, Deserialize)]
11#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
12pub struct Auth {
13	actor: Actor,
14}
15
16impl Auth {
17	pub fn new(actor: Actor) -> Self {
18		Self {
19			actor,
20		}
21	}
22
23	pub fn id(&self) -> &str {
24		self.actor.id()
25	}
26
27	/// Return current authentication level
28	pub fn level(&self) -> &Level {
29		self.actor.level()
30	}
31
32	/// Check if the current auth is anonymous
33	pub fn is_anon(&self) -> bool {
34		matches!(self.level(), Level::No)
35	}
36
37	/// Check if the current level is Root
38	pub fn is_root(&self) -> bool {
39		matches!(self.level(), Level::Root)
40	}
41
42	/// Check if the current level is Namespace
43	pub fn is_ns(&self) -> bool {
44		matches!(self.level(), Level::Namespace(_))
45	}
46
47	/// Check if the current level is Database
48	pub fn is_db(&self) -> bool {
49		matches!(self.level(), Level::Database(_, _))
50	}
51
52	/// Check if the current level is Record
53	pub fn is_record(&self) -> bool {
54		matches!(self.level(), Level::Record(_, _, _))
55	}
56
57	/// Check if the current level is Namespace, and the namespace matches
58	pub fn is_ns_check(&self, ns: &str) -> bool {
59		matches!(self.level(), Level::Namespace(n) if n.eq(ns))
60	}
61
62	/// Check if the current level is Database, and the namespace and database
63	/// match
64	pub fn is_db_check(&self, ns: &str, db: &str) -> bool {
65		matches!(self.level(), Level::Database(n, d) if n.eq(ns) && d.eq(db))
66	}
67
68	/// Check whether the authenticated level is permitted to operate within the
69	/// given namespace and database.
70	///
71	/// This is the tenant-boundary gate for entry points that derive the target
72	/// namespace/database from caller-controlled input — most notably the custom
73	/// API HTTP route `/api/:ns/:db/:endpoint`, whose handlers run with
74	/// permissions disabled. The authenticated [`Level`] is the source of truth;
75	/// the session's *selected* `ns`/`db` are not, because they are overwritten
76	/// from the request before dispatch.
77	///
78	/// - Root principals may act in any namespace/database.
79	/// - Namespace principals are confined to their own namespace (any database).
80	/// - Database and record principals are confined to their exact namespace/database.
81	/// - Anonymous (unauthenticated) sessions carry no tenant identity, so they are left to the
82	///   endpoint's own permission checks.
83	///
84	/// This is purely a namespace/database scope check; it does not replace
85	/// role or `PERMISSIONS` evaluation.
86	pub fn can_access_ns_db(&self, ns: &str, db: &str) -> bool {
87		match self.level() {
88			Level::Root => true,
89			Level::Namespace(n) => n.eq(ns),
90			Level::Database(n, d) => n.eq(ns) && d.eq(db),
91			Level::Record(n, d, _) => n.eq(ns) && d.eq(db),
92			Level::No => true,
93		}
94	}
95
96	/// System Auth helpers
97	///
98	/// These are not stored in the database and are used for internal
99	/// operations Do not use for authentication
100	pub fn for_root(role: Role) -> Self {
101		Self::new(Actor::new("system_auth".into(), vec![role], Level::Root))
102	}
103
104	pub fn for_ns(role: Role, ns: &str) -> Self {
105		Self::new(Actor::new("system_auth".into(), vec![role], Level::Namespace(ns.to_owned())))
106	}
107
108	pub fn for_db(role: Role, ns: &str, db: &str) -> Self {
109		Self::new(Actor::new(
110			"system_auth".into(),
111			vec![role],
112			Level::Database(ns.to_owned(), db.to_owned()),
113		))
114	}
115
116	pub fn for_record(rid: String, ns: &str, db: &str, ac: &str) -> Self {
117		Self::new(Actor::new(
118			rid,
119			vec![],
120			Level::Record(ns.to_owned(), db.to_owned(), ac.to_owned()),
121		))
122	}
123
124	pub fn new_limited(&self, limit: &AuthLimit) -> Self {
125		Self::new(self.actor.new_limited(limit))
126	}
127
128	pub fn max_role(&self) -> Option<Role> {
129		self.actor.max_role()
130	}
131
132	//
133	// Permission checks
134	//
135
136	/// Checks if the current auth is allowed to perform an action on a given
137	/// resource
138	pub fn is_allowed(&self, action: Action, res: &Resource) -> Result<()> {
139		is_allowed(&self.actor, &action, res)
140			.map_err(crate::err::Error::from)
141			.map_err(anyhow::Error::new)
142	}
143
144	/// Checks if the current actor has a given role
145	pub fn has_role(&self, role: Role) -> bool {
146		self.actor.has_role(role)
147	}
148
149	/// Checks if the current actor has a Owner role
150	pub fn has_owner_role(&self) -> bool {
151		self.actor.has_owner_role()
152	}
153
154	/// Checks if the current actor has a Editor role
155	pub fn has_editor_role(&self) -> bool {
156		self.actor.has_editor_role()
157	}
158
159	/// Checks if the current actor has a Viewer role
160	pub fn has_viewer_role(&self) -> bool {
161		self.actor.has_viewer_role()
162	}
163}
164
165#[cfg(test)]
166mod tests {
167	use super::*;
168
169	#[test]
170	fn can_access_ns_db_enforces_tenant_boundary() {
171		// Root may access any namespace/database.
172		let root = Auth::for_root(Role::Viewer);
173		assert!(root.can_access_ns_db("a", "x"));
174		assert!(root.can_access_ns_db("b", "y"));
175
176		// Namespace principals are confined to their namespace (any database).
177		let ns = Auth::for_ns(Role::Viewer, "a");
178		assert!(ns.can_access_ns_db("a", "x"));
179		assert!(ns.can_access_ns_db("a", "y"));
180		assert!(!ns.can_access_ns_db("b", "x"));
181
182		// Database principals are confined to their exact namespace/database.
183		let db = Auth::for_db(Role::Viewer, "a", "x");
184		assert!(db.can_access_ns_db("a", "x"));
185		assert!(!db.can_access_ns_db("a", "y"));
186		assert!(!db.can_access_ns_db("b", "x"));
187
188		// Record principals are confined to their namespace/database.
189		let rec = Auth::for_record("user:1".to_string(), "a", "x", "ac");
190		assert!(rec.can_access_ns_db("a", "x"));
191		assert!(!rec.can_access_ns_db("a", "y"));
192		assert!(!rec.can_access_ns_db("b", "x"));
193
194		// Anonymous sessions carry no tenant identity; the scope test is
195		// permissive and the endpoint's own permission checks remain the gate.
196		assert!(Auth::default().can_access_ns_db("a", "x"));
197	}
198}