1use std::fmt;
2use std::str::FromStr;
3use std::sync::Arc;
4
5use anyhow::Result;
6use chrono::Utc;
7use revision::revisioned;
8use serde::{Deserialize, Serialize};
9use surrealdb_types::ToSql;
10use uuid::Uuid;
11
12use crate::iam::{Auth, Level, Role};
13use crate::kvs::impl_kv_value_revisioned;
14use crate::types::{PublicValue, PublicVariables};
15use crate::val::{Object, Value};
16
17#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
26pub struct Session {
27 pub au: Arc<Auth>,
29 pub rt: bool,
31 pub ip: Option<String>,
33 pub or: Option<String>,
35 pub id: Option<Uuid>,
37 pub ns: Option<String>,
39 pub db: Option<String>,
41 pub ac: Option<String>,
43 pub tk: Option<PublicValue>,
45 pub rd: Option<PublicValue>,
47 pub exp: Option<i64>,
49 pub variables: PublicVariables,
51 pub new_planner_strategy: NewPlannerStrategy,
53 pub redact_volatile_explain_attrs: bool,
56}
57
58#[revisioned(revision = 1)]
59#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash, Serialize, Deserialize)]
60pub enum NewPlannerStrategy {
61 #[default]
63 BestEffortReadOnlyStatements,
64 ComputeOnly,
66 AllReadOnlyStatements,
69}
70
71impl fmt::Display for NewPlannerStrategy {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Self::BestEffortReadOnlyStatements => f.write_str("best-effort"),
75 Self::ComputeOnly => f.write_str("compute-only"),
76 Self::AllReadOnlyStatements => f.write_str("all-read-only"),
77 }
78 }
79}
80
81impl FromStr for NewPlannerStrategy {
82 type Err = String;
83
84 fn from_str(s: &str) -> Result<Self, Self::Err> {
85 match s {
86 "best-effort" => Ok(Self::BestEffortReadOnlyStatements),
87 "compute-only" => Ok(Self::ComputeOnly),
88 "all-read-only" => Ok(Self::AllReadOnlyStatements),
89 _ => Err(format!(
90 "unknown planner strategy: '{s}' (expected 'best-effort', 'compute-only', or 'all-read-only')"
91 )),
92 }
93 }
94}
95
96impl Session {
97 pub fn with_ns(mut self, ns: &str) -> Session {
99 self.ns = Some(ns.to_owned());
100 self
101 }
102
103 pub fn with_db(mut self, db: &str) -> Session {
105 self.db = Some(db.to_owned());
106 self
107 }
108
109 pub fn with_ac(mut self, ac: &str) -> Session {
111 self.ac = Some(ac.to_owned());
112 self
113 }
114
115 pub fn with_rt(mut self, rt: bool) -> Session {
117 self.rt = rt;
118 self
119 }
120
121 pub fn new_planner_strategy(mut self, strategy: NewPlannerStrategy) -> Session {
123 self.new_planner_strategy = strategy;
124 self
125 }
126
127 pub(crate) fn ns(&self) -> Option<Arc<str>> {
129 self.ns.as_deref().map(Into::into)
130 }
131
132 pub(crate) fn db(&self) -> Option<Arc<str>> {
134 self.db.as_deref().map(Into::into)
135 }
136
137 pub(crate) fn live(&self) -> bool {
139 self.rt
140 }
141
142 pub(crate) fn expired(&self) -> bool {
144 match self.exp {
145 Some(exp) => Utc::now().timestamp() > exp,
146 None => false,
148 }
149 }
150
151 pub(crate) fn values(&self) -> Vec<(&'static str, Value)> {
152 use crate::sql::expression::convert_public_value_to_internal;
153
154 let access = self.ac.as_deref().map(Value::from).unwrap_or(Value::None);
155 let auth = self.rd.clone().map(convert_public_value_to_internal).unwrap_or(Value::None);
156 let token = self.tk.clone().map(convert_public_value_to_internal).unwrap_or(Value::None);
157 let session = Value::from(map! {
158 "ac" => access.clone(),
159 "exp" => self.exp.map(Value::from).unwrap_or(Value::None),
160 "db" => self.db.as_deref().map(Value::from).unwrap_or(Value::None),
161 "id" => self.id.map(Value::from).unwrap_or(Value::None),
162 "ip" => self.ip.as_deref().map(Value::from).unwrap_or(Value::None),
163 "ns" => self.ns.as_deref().map(Value::from).unwrap_or(Value::None),
164 "or" => self.or.as_deref().map(Value::from).unwrap_or(Value::None),
165 "rd" => auth.clone(),
166 "tk" => token.clone(),
167 });
168
169 vec![("access", access), ("auth", auth), ("token", token), ("session", session)]
170 }
171
172 pub fn for_level(level: Level, role: Role) -> Session {
174 let mut sess = Session::default();
176 match level {
178 Level::Root => {
179 sess.au = Arc::new(Auth::for_root(role));
180 }
181 Level::Namespace(ns) => {
182 sess.au = Arc::new(Auth::for_ns(role, &ns));
183 sess.ns = Some(ns);
184 }
185 Level::Database(ns, db) => {
186 sess.au = Arc::new(Auth::for_db(role, &ns, &db));
187 sess.ns = Some(ns);
188 sess.db = Some(db);
189 }
190 _ => {}
191 }
192 sess
193 }
194
195 pub fn for_record(ns: &str, db: &str, ac: &str, rid: PublicValue) -> Session {
197 Session {
198 ac: Some(ac.to_owned()),
199 au: Arc::new(Auth::for_record(rid.to_sql(), ns, db, ac)),
200 rt: false,
201 ip: None,
202 or: None,
203 id: None,
204 ns: Some(ns.to_owned()),
205 db: Some(db.to_owned()),
206 tk: None,
207 rd: Some(rid),
208 exp: None,
209 variables: Default::default(),
210 new_planner_strategy: NewPlannerStrategy::default(),
211 redact_volatile_explain_attrs: false,
212 }
213 }
214
215 pub fn owner() -> Session {
217 Session::for_level(Level::Root, Role::Owner)
218 }
219
220 pub fn editor() -> Session {
222 Session::for_level(Level::Root, Role::Editor)
223 }
224
225 pub fn viewer() -> Session {
227 Session::for_level(Level::Root, Role::Viewer)
228 }
229}
230
231#[revisioned(revision = 1)]
246#[derive(Clone, Debug, PartialEq)]
247pub(crate) struct DurableSession {
248 pub(crate) expires_at: u64,
253 pub(crate) au: Auth,
255 pub(crate) rt: bool,
257 pub(crate) ip: Option<String>,
259 pub(crate) or: Option<String>,
261 pub(crate) id: Option<Uuid>,
263 pub(crate) ns: Option<String>,
265 pub(crate) db: Option<String>,
267 pub(crate) ac: Option<String>,
269 pub(crate) tk: Option<Value>,
271 pub(crate) rd: Option<Value>,
273 pub(crate) exp: Option<i64>,
275 pub(crate) variables: Object,
277 pub(crate) new_planner_strategy: NewPlannerStrategy,
279 pub(crate) redact_volatile_explain_attrs: bool,
281}
282
283impl_kv_value_revisioned!(DurableSession);
284
285impl DurableSession {
286 pub(crate) fn from_session(session: &Session, expires_at: u64) -> Self {
289 use crate::sql::expression::convert_public_value_to_internal;
290 Self {
291 expires_at,
292 au: (*session.au).clone(),
293 rt: session.rt,
294 ip: session.ip.clone(),
295 or: session.or.clone(),
296 id: session.id,
297 ns: session.ns.clone(),
298 db: session.db.clone(),
299 ac: session.ac.clone(),
300 tk: session.tk.clone().map(convert_public_value_to_internal),
301 rd: session.rd.clone().map(convert_public_value_to_internal),
302 exp: session.exp,
303 variables: session
304 .variables
305 .clone()
306 .into_iter()
307 .map(|(k, v)| (k, convert_public_value_to_internal(v)))
308 .collect(),
309 new_planner_strategy: session.new_planner_strategy,
310 redact_volatile_explain_attrs: session.redact_volatile_explain_attrs,
311 }
312 }
313
314 pub(crate) fn into_session(self) -> Result<Session> {
316 use crate::val::convert_value_to_public_value;
317 Ok(Session {
318 au: Arc::new(self.au),
319 rt: self.rt,
320 ip: self.ip,
321 or: self.or,
322 id: self.id,
323 ns: self.ns,
324 db: self.db,
325 ac: self.ac,
326 tk: self.tk.map(convert_value_to_public_value).transpose()?,
327 rd: self.rd.map(convert_value_to_public_value).transpose()?,
328 exp: self.exp,
329 variables: self
330 .variables
331 .into_iter()
332 .map(|(k, v)| Ok((k.into_string(), convert_value_to_public_value(v)?)))
333 .collect::<Result<PublicVariables>>()?,
334 new_planner_strategy: self.new_planner_strategy,
335 redact_volatile_explain_attrs: self.redact_volatile_explain_attrs,
336 })
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343
344 #[test]
345 fn json_round_trip_preserves_auth_and_context() {
346 let original = Session {
349 id: Some(Uuid::from_u128(1)),
350 exp: Some(1_700_000_000),
351 ..Session::owner().with_ns("app").with_db("app")
352 };
353
354 let json = serde_json::to_string(&original).expect("serialize");
355 let restored: Session = serde_json::from_str(&json).expect("deserialize");
356
357 assert_eq!(original, restored);
360 assert!(restored.au.is_root());
361 assert_eq!(restored.ns.as_deref(), Some("app"));
362 assert_eq!(restored.db.as_deref(), Some("app"));
363 }
364
365 #[test]
371 fn durable_session_round_trip_preserves_all_fields() {
372 use std::collections::BTreeMap;
373
374 use surrealdb_types::{Number, Value as PV};
375
376 let mut variables = PublicVariables::default();
377 variables.insert("str", PV::String("hello".to_owned()));
378 variables.insert("dec", PV::Number(Number::Decimal("1.5".parse().unwrap())));
379 variables.insert("rid", PV::RecordId(surrealdb_types::RecordId::new("person", "tobie")));
380 variables.insert("dt", PV::Datetime(surrealdb_types::Datetime::now()));
381 variables.insert(
382 "obj",
383 PV::Object(surrealdb_types::Object::from(BTreeMap::from([("nested", PV::Bool(true))]))),
384 );
385
386 let original = Session {
387 rt: true,
388 ip: Some("10.0.0.1".to_owned()),
389 or: Some("example.com".to_owned()),
390 id: Some(Uuid::from_u128(7)),
391 exp: Some(1_700_000_000),
392 tk: Some(PV::Object(surrealdb_types::Object::from(BTreeMap::from([(
393 "iss",
394 PV::String("surrealdb".to_owned()),
395 )])))),
396 rd: Some(PV::RecordId(surrealdb_types::RecordId::new("person", "tobie"))),
397 variables,
398 redact_volatile_explain_attrs: true,
399 ..Session::for_record(
400 "app",
401 "app",
402 "account",
403 PublicValue::RecordId(surrealdb_types::RecordId::new("person", "tobie")),
404 )
405 };
406
407 let durable = DurableSession::from_session(&original, 123_456_789);
408 assert_eq!(durable.expires_at, 123_456_789);
409
410 let bytes = revision::to_vec(&durable).expect("revision encode");
412 let decoded: DurableSession = revision::from_slice(&bytes).expect("revision decode");
413 assert_eq!(durable, decoded);
414
415 let restored = decoded.into_session().expect("convert back");
416 assert_eq!(original, restored);
417 }
418}