1use super::access::{authenticate_record, create_refresh_token_record};
2use crate::cnf::{INSECURE_FORWARD_ACCESS_ERRORS, SERVER_NAME};
3use crate::dbs::capabilities::ExperimentalTarget;
4use crate::dbs::Session;
5use crate::err::Error;
6use crate::iam::issue::{config, expiration};
7use crate::iam::token::Claims;
8use crate::iam::Auth;
9use crate::iam::{Actor, Level};
10use crate::kvs::{Datastore, LockType::*, TransactionType::*};
11use crate::sql::AccessType;
12use crate::sql::Object;
13use crate::sql::Value;
14use chrono::Utc;
15use jsonwebtoken::{encode, Header};
16use revision::revisioned;
17use serde::{Deserialize, Serialize};
18use std::sync::Arc;
19use uuid::Uuid;
20
21#[revisioned(revision = 1)]
22#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Hash)]
23#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
24#[non_exhaustive]
25pub struct SignupData {
26 pub token: Option<String>,
27 pub refresh: Option<String>,
28}
29
30impl From<SignupData> for Value {
31 fn from(v: SignupData) -> Value {
32 let mut out = Object::default();
33 if let Some(token) = v.token {
34 out.insert("token".to_string(), token.into());
35 }
36 if let Some(refresh) = v.refresh {
37 out.insert("refresh".to_string(), refresh.into());
38 }
39 out.into()
40 }
41}
42
43pub async fn signup(
44 kvs: &Datastore,
45 session: &mut Session,
46 vars: Object,
47) -> Result<SignupData, Error> {
48 vars.validate_computed()?;
50 let ns = vars.get("NS").or_else(|| vars.get("ns"));
52 let db = vars.get("DB").or_else(|| vars.get("db"));
53 let ac = vars.get("AC").or_else(|| vars.get("ac"));
54 match (ns, db, ac) {
56 (Some(ns), Some(db), Some(ac)) => {
57 let ns = ns.to_raw_string();
59 let db = db.to_raw_string();
60 let ac = ac.to_raw_string();
61 super::signup::db_access(kvs, session, ns, db, ac, vars).await
64 }
65 _ => Err(Error::InvalidSignup),
66 }
67}
68
69pub async fn db_access(
70 kvs: &Datastore,
71 session: &mut Session,
72 ns: String,
73 db: String,
74 ac: String,
75 vars: Object,
76) -> Result<SignupData, Error> {
77 let tx = kvs.transaction(Read, Optimistic).await?;
79 let access = tx.get_db_access(&ns, &db, &ac).await;
81 tx.cancel().await?;
83 match access {
85 Ok(av) => {
86 match &av.kind {
89 AccessType::Record(at) => {
90 let iss = match &at.jwt.issue {
92 Some(iss) => iss,
93 _ => return Err(Error::AccessMethodMismatch),
94 };
95 match &at.signup {
96 Some(val) => {
98 let vars = Some(vars.0);
100 let mut sess = Session::editor().with_ns(&ns).with_db(&db);
102 sess.ip.clone_from(&session.ip);
103 sess.or.clone_from(&session.or);
104 match kvs.evaluate(val, &sess, vars).await {
106 Ok(val) => {
108 match val.record() {
109 Some(mut rid) => {
111 let key = config(iss.alg, &iss.key)?;
113 let claims = Claims {
115 iss: Some(SERVER_NAME.to_owned()),
116 iat: Some(Utc::now().timestamp()),
117 nbf: Some(Utc::now().timestamp()),
118 exp: expiration(av.duration.token)?,
119 jti: Some(Uuid::new_v4().to_string()),
120 ns: Some(ns.to_owned()),
121 db: Some(db.to_owned()),
122 ac: Some(ac.to_owned()),
123 id: Some(rid.to_raw()),
124 ..Claims::default()
125 };
126 if let Some(au) = &av.authenticate {
128 let mut sess =
130 Session::editor().with_ns(&ns).with_db(&db);
131 sess.rd = Some(rid.clone().into());
132 sess.tk = Some((&claims).into());
133 sess.ip.clone_from(&session.ip);
134 sess.or.clone_from(&session.or);
135 rid = authenticate_record(kvs, &sess, au).await?;
136 }
137 let refresh = match &at.bearer {
139 Some(_) => {
140 if !kvs.get_capabilities().allows_experimental(
142 &ExperimentalTarget::BearerAccess,
143 ) {
144 debug!("Will not create refresh token with disabled bearer access feature");
145 None
146 } else {
147 Some(
148 create_refresh_token_record(
149 kvs,
150 av.name.clone(),
151 &ns,
152 &db,
153 rid.clone(),
154 )
155 .await?,
156 )
157 }
158 }
159 None => None,
160 };
161 trace!("Signing up with access method `{}`", ac);
163 let enc =
165 encode(&Header::new(iss.alg.into()), &claims, &key);
166 session.tk = Some((&claims).into());
168 session.ns = Some(ns.to_owned());
169 session.db = Some(db.to_owned());
170 session.ac = Some(ac.to_owned());
171 session.rd = Some(Value::from(rid.to_owned()));
172 session.exp = expiration(av.duration.session)?;
173 session.au = Arc::new(Auth::new(Actor::new(
174 rid.to_string(),
175 Default::default(),
176 Level::Record(ns, db, rid.to_string()),
177 )));
178 match enc {
180 Ok(token) => Ok(SignupData {
182 token: Some(token),
183 refresh,
184 }),
185 _ => Err(Error::TokenMakingFailed),
186 }
187 }
188 _ => Err(Error::NoRecordFound),
189 }
190 }
191 Err(e) => match e {
192 Error::Thrown(_) => Err(e),
194 Error::Tx(_) | Error::TxFailure | Error::TxRetryable => {
197 debug!("Unexpected error found while executing a SIGNUP clause: {e}");
198 Err(Error::UnexpectedAuth)
199 }
200 e => {
202 debug!("Record user signup query failed: {e}");
203 if *INSECURE_FORWARD_ACCESS_ERRORS {
204 Err(e)
205 } else {
206 Err(Error::AccessRecordSignupQueryFailed)
207 }
208 }
209 },
210 }
211 }
212 _ => Err(Error::AccessRecordNoSignup),
213 }
214 }
215 _ => Err(Error::AccessMethodMismatch),
216 }
217 }
218 _ => Err(Error::AccessNotFound),
219 }
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use crate::{dbs::Capabilities, iam::Role};
226 use chrono::Duration;
227 use std::collections::HashMap;
228
229 #[tokio::test]
230 async fn test_record_signup() {
231 {
233 let ds = Datastore::new("memory").await.unwrap();
234 let sess = Session::owner().with_ns("test").with_db("test");
235 ds.execute(
236 r#"
237 DEFINE ACCESS user ON DATABASE TYPE RECORD
238 SIGNIN (
239 SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
240 )
241 SIGNUP (
242 CREATE user CONTENT {
243 name: $user,
244 pass: crypto::argon2::generate($pass)
245 }
246 )
247 DURATION FOR SESSION 2h
248 ;
249 "#,
250 &sess,
251 None,
252 )
253 .await
254 .unwrap();
255
256 let mut sess = Session {
258 ns: Some("test".to_string()),
259 db: Some("test".to_string()),
260 ..Default::default()
261 };
262 let mut vars: HashMap<&str, Value> = HashMap::new();
263 vars.insert("user", "user".into());
264 vars.insert("pass", "pass".into());
265 let res = db_access(
266 &ds,
267 &mut sess,
268 "test".to_string(),
269 "test".to_string(),
270 "user".to_string(),
271 vars.into(),
272 )
273 .await;
274
275 assert!(res.is_ok(), "Failed to signup: {:?}", res);
276 assert_eq!(sess.ns, Some("test".to_string()));
277 assert_eq!(sess.db, Some("test".to_string()));
278 assert!(sess.au.id().starts_with("user:"));
279 assert!(sess.au.is_record());
280 assert_eq!(sess.au.level().ns(), Some("test"));
281 assert_eq!(sess.au.level().db(), Some("test"));
282 assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
284 assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
285 assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
286 let exp = sess.exp.unwrap();
288 let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
290 let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
291 assert!(
292 exp > min_exp && exp < max_exp,
293 "Session expiration is expected to match the defined duration"
294 );
295 }
296
297 {
299 let ds = Datastore::new("memory").await.unwrap();
300 let sess = Session::owner().with_ns("test").with_db("test");
301 ds.execute(
302 r#"
303 DEFINE ACCESS user ON DATABASE TYPE RECORD
304 SIGNIN (
305 SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
306 )
307 SIGNUP (
308 CREATE user CONTENT {
309 name: $user,
310 pass: crypto::argon2::generate($pass)
311 }
312 )
313 DURATION FOR SESSION 2h
314 ;
315 "#,
316 &sess,
317 None,
318 )
319 .await
320 .unwrap();
321
322 let mut sess = Session {
324 ns: Some("test".to_string()),
325 db: Some("test".to_string()),
326 ..Default::default()
327 };
328 let mut vars: HashMap<&str, Value> = HashMap::new();
329 vars.insert("user", "user".into());
331 let res = db_access(
332 &ds,
333 &mut sess,
334 "test".to_string(),
335 "test".to_string(),
336 "user".to_string(),
337 vars.into(),
338 )
339 .await;
340
341 assert!(res.is_err(), "Unexpected successful signup: {:?}", res);
342 }
343 }
344
345 #[tokio::test]
346 async fn test_signup_record_with_refresh() {
347 use crate::iam::signin;
348
349 {
351 let ds = Datastore::new("memory").await.unwrap();
352 let sess = Session::owner().with_ns("test").with_db("test");
353 ds.execute(
354 r#"
355 DEFINE ACCESS user ON DATABASE TYPE RECORD
356 SIGNUP (
357 CREATE user CONTENT {
358 name: $user,
359 pass: crypto::argon2::generate($pass)
360 }
361 )
362 DURATION FOR GRANT 1w, FOR SESSION 2h
363 ;
364
365 CREATE user:test CONTENT {
366 name: 'user',
367 pass: crypto::argon2::generate('pass')
368 }
369 "#,
370 &sess,
371 None,
372 )
373 .await
374 .unwrap();
375
376 let mut sess = Session {
378 ns: Some("test".to_string()),
379 db: Some("test".to_string()),
380 ..Default::default()
381 };
382 let mut vars: HashMap<&str, Value> = HashMap::new();
383 vars.insert("user", "user".into());
384 vars.insert("pass", "pass".into());
385 let res = db_access(
386 &ds,
387 &mut sess,
388 "test".to_string(),
389 "test".to_string(),
390 "user".to_string(),
391 vars.into(),
392 )
393 .await;
394
395 match res {
396 Ok(data) => {
397 assert!(data.refresh.is_none(), "Refresh token was unexpectedly returned")
398 }
399 Err(e) => panic!("Failed to signup with credentials: {e}"),
400 }
401 }
402 {
404 let ds = Datastore::new("memory").await.unwrap().with_capabilities(
405 Capabilities::default().with_experimental(ExperimentalTarget::BearerAccess.into()),
406 );
407 let sess = Session::owner().with_ns("test").with_db("test");
408 ds.execute(
409 r#"
410 DEFINE ACCESS user ON DATABASE TYPE RECORD
411 SIGNUP (
412 CREATE user CONTENT {
413 name: $user,
414 pass: crypto::argon2::generate($pass)
415 }
416 )
417 WITH REFRESH
418 DURATION FOR GRANT 1w, FOR SESSION 2h
419 ;
420
421 CREATE user:test CONTENT {
422 name: 'user',
423 pass: crypto::argon2::generate('pass')
424 }
425 "#,
426 &sess,
427 None,
428 )
429 .await
430 .unwrap();
431
432 let mut sess = Session {
434 ns: Some("test".to_string()),
435 db: Some("test".to_string()),
436 ..Default::default()
437 };
438 let mut vars: HashMap<&str, Value> = HashMap::new();
439 vars.insert("user", "user".into());
440 vars.insert("pass", "pass".into());
441 let res = db_access(
442 &ds,
443 &mut sess,
444 "test".to_string(),
445 "test".to_string(),
446 "user".to_string(),
447 vars.into(),
448 )
449 .await;
450
451 assert!(res.is_ok(), "Failed to signup with credentials: {:?}", res);
452 let refresh = match res {
453 Ok(data) => match data.refresh {
454 Some(refresh) => refresh,
455 None => panic!("Refresh token was not returned"),
456 },
457 Err(e) => panic!("Failed to signup with credentials: {e}"),
458 };
459 assert_eq!(sess.ns, Some("test".to_string()));
460 assert_eq!(sess.db, Some("test".to_string()));
461 assert!(sess.au.id().starts_with("user:"));
462 assert!(sess.au.is_record());
463 assert_eq!(sess.au.level().ns(), Some("test"));
464 assert_eq!(sess.au.level().db(), Some("test"));
465 assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
467 assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
468 assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
469 let exp = sess.exp.unwrap();
471 let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
473 let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
474 assert!(
475 exp > min_exp && exp < max_exp,
476 "Session expiration is expected to follow the defined duration"
477 );
478 let mut vars: HashMap<&str, Value> = HashMap::new();
480 vars.insert("refresh", refresh.clone().into());
481 let res = signin::db_access(
482 &ds,
483 &mut sess,
484 "test".to_string(),
485 "test".to_string(),
486 "user".to_string(),
487 vars.into(),
488 )
489 .await;
490 match res {
492 Ok(data) => match data.refresh {
493 Some(new_refresh) => assert!(
494 new_refresh != refresh,
495 "New refresh token is identical to used one"
496 ),
497 None => panic!("Refresh token was not returned"),
498 },
499 Err(e) => panic!("Failed to signin with credentials: {e}"),
500 };
501 assert_eq!(sess.ns, Some("test".to_string()));
502 assert_eq!(sess.db, Some("test".to_string()));
503 assert!(sess.au.id().starts_with("user:"));
504 assert!(sess.au.is_record());
505 assert_eq!(sess.au.level().ns(), Some("test"));
506 assert_eq!(sess.au.level().db(), Some("test"));
507 assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
509 assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
510 assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
511 let exp = sess.exp.unwrap();
513 let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
515 let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
516 assert!(
517 exp > min_exp && exp < max_exp,
518 "Session expiration is expected to follow the defined duration"
519 );
520 let mut vars: HashMap<&str, Value> = HashMap::new();
522 vars.insert("refresh", refresh.into());
523 let res = signin::db_access(
524 &ds,
525 &mut sess,
526 "test".to_string(),
527 "test".to_string(),
528 "user".to_string(),
529 vars.into(),
530 )
531 .await;
532 match res {
533 Ok(data) => panic!("Unexpected successful signin: {:?}", data),
534 Err(Error::InvalidAuth) => {} Err(e) => panic!("Expected InvalidAuth, but got: {e}"),
536 }
537 }
538 }
539
540 #[tokio::test]
541 async fn test_record_signup_with_jwt_issuer() {
542 use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
543 {
545 let public_key = r#"-----BEGIN PUBLIC KEY-----
546MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
5474lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
548+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
549kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
5500iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
551cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
552mwIDAQAB
553-----END PUBLIC KEY-----"#;
554 let private_key = r#"-----BEGIN PRIVATE KEY-----
555MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
556MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
557NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
558qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
559p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
560ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
561VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
562laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
563sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
564mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
565dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
566ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
567DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
568N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
5690l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
570t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
571AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
57248RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
573DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
574xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
575mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
5762bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
577et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
578VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
579TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
580dn/RsYEONbwQSjIfMPkvxF+8HQ==
581-----END PRIVATE KEY-----"#;
582 let ds = Datastore::new("memory").await.unwrap();
583 let sess = Session::owner().with_ns("test").with_db("test");
584 ds.execute(
585 &format!(
586 r#"
587 DEFINE ACCESS user ON DATABASE TYPE RECORD
588 SIGNIN (
589 SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
590 )
591 SIGNUP (
592 CREATE user CONTENT {{
593 name: $user,
594 pass: crypto::argon2::generate($pass)
595 }}
596 )
597 WITH JWT ALGORITHM RS256 KEY '{public_key}'
598 WITH ISSUER KEY '{private_key}'
599 DURATION FOR SESSION 2h, FOR TOKEN 15m
600 ;
601
602 CREATE user:test CONTENT {{
603 name: 'user',
604 pass: crypto::argon2::generate('pass')
605 }}
606 "#
607 ),
608 &sess,
609 None,
610 )
611 .await
612 .unwrap();
613
614 let mut sess = Session {
616 ns: Some("test".to_string()),
617 db: Some("test".to_string()),
618 ..Default::default()
619 };
620 let mut vars: HashMap<&str, Value> = HashMap::new();
621 vars.insert("user", "user".into());
622 vars.insert("pass", "pass".into());
623 let res = db_access(
624 &ds,
625 &mut sess,
626 "test".to_string(),
627 "test".to_string(),
628 "user".to_string(),
629 vars.into(),
630 )
631 .await;
632
633 assert!(res.is_ok(), "Failed to signup: {:?}", res);
634 assert_eq!(sess.ns, Some("test".to_string()));
635 assert_eq!(sess.db, Some("test".to_string()));
636 assert!(sess.au.id().starts_with("user:"));
637 assert!(sess.au.is_record());
638 assert_eq!(sess.au.level().ns(), Some("test"));
639 assert_eq!(sess.au.level().db(), Some("test"));
640 assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
642 assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
643 assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
644 let exp = sess.exp.unwrap();
646 let min_sess_exp =
648 (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
649 let max_sess_exp =
650 (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
651 assert!(
652 exp > min_sess_exp && exp < max_sess_exp,
653 "Session expiration is expected to follow access method duration"
654 );
655
656 if let Ok(SignupData {
658 token: Some(tk),
659 ..
660 }) = res
661 {
662 let val = Validation::new(Algorithm::RS256);
664 let token_data = decode::<Claims>(
666 &tk,
667 &DecodingKey::from_rsa_pem(public_key.as_ref()).unwrap(),
668 &val,
669 )
670 .unwrap();
671 assert_eq!(token_data.header.alg, Algorithm::RS256);
673 let exp = match token_data.claims.exp {
676 Some(exp) => exp,
677 _ => panic!("Token is missing expiration claim"),
678 };
679 let min_tk_exp =
680 (Utc::now() + Duration::minutes(15) - Duration::seconds(10)).timestamp();
681 let max_tk_exp =
682 (Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
683 assert!(
684 exp > min_tk_exp && exp < max_tk_exp,
685 "Token expiration is expected to follow issuer duration"
686 );
687 assert_eq!(token_data.claims.ns, Some("test".to_string()));
689 assert_eq!(token_data.claims.db, Some("test".to_string()));
690 assert!(token_data.claims.id.unwrap().starts_with("user:"));
691 assert_eq!(token_data.claims.ac, Some("user".to_string()));
692 } else {
693 panic!("Token could not be extracted from result")
694 }
695 }
696 }
697
698 #[tokio::test]
699 async fn test_signup_record_and_authenticate_clause() {
700 {
702 let ds = Datastore::new("memory").await.unwrap();
703 let sess = Session::owner().with_ns("test").with_db("test");
704 ds.execute(
705 r#"
706 DEFINE ACCESS user ON DATABASE TYPE RECORD
707 SIGNUP (
708 CREATE type::thing('user', $id)
709 )
710 AUTHENTICATE (
711 -- Simple example increasing the record identifier by one
712 SELECT * FROM type::thing('user', record::id($auth) + 1)
713 )
714 DURATION FOR SESSION 2h
715 ;
716
717 CREATE user:2;
718 "#,
719 &sess,
720 None,
721 )
722 .await
723 .unwrap();
724
725 let mut sess = Session {
727 ns: Some("test".to_string()),
728 db: Some("test".to_string()),
729 ..Default::default()
730 };
731 let mut vars: HashMap<&str, Value> = HashMap::new();
732 vars.insert("id", 1.into());
733 let res = db_access(
734 &ds,
735 &mut sess,
736 "test".to_string(),
737 "test".to_string(),
738 "user".to_string(),
739 vars.into(),
740 )
741 .await;
742
743 assert!(res.is_ok(), "Failed to signup with credentials: {:?}", res);
744 assert_eq!(sess.ns, Some("test".to_string()));
745 assert_eq!(sess.db, Some("test".to_string()));
746 assert_eq!(sess.ac, Some("user".to_string()));
747 assert_eq!(sess.au.id(), "user:2");
748 assert!(sess.au.is_record());
749 assert_eq!(sess.au.level().ns(), Some("test"));
750 assert_eq!(sess.au.level().db(), Some("test"));
751 assert_eq!(sess.au.level().id(), Some("user:2"));
752 assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
754 assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
755 assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
756 let exp = sess.exp.unwrap();
758 let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
760 let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
761 assert!(
762 exp > min_exp && exp < max_exp,
763 "Session expiration is expected to follow the defined duration"
764 );
765 }
766
767 {
769 let ds = Datastore::new("memory").await.unwrap();
770 let sess = Session::owner().with_ns("test").with_db("test");
771 ds.execute(
772 r#"
773 DEFINE ACCESS owner ON DATABASE TYPE RECORD
774 SIGNUP (
775 -- Allow anyone to sign up as a new company
776 -- This automatically creates an owner with the same credentials
777 CREATE company CONTENT {
778 email: $email,
779 pass: crypto::argon2::generate($pass),
780 owner: (CREATE employee CONTENT {
781 email: $email,
782 pass: $pass,
783 }),
784 }
785 )
786 SIGNIN (
787 -- Allow company owners to log in directly with the company account
788 SELECT * FROM company WHERE email = $email AND crypto::argon2::compare(pass, $pass)
789 )
790 AUTHENTICATE (
791 -- If logging in with a company account, the session will be authenticated as the first owner
792 IF record::tb($auth) = "company" {
793 RETURN SELECT VALUE owner FROM company WHERE id = $auth
794 }
795 )
796 DURATION FOR SESSION 2h
797 ;
798
799 CREATE company:1 CONTENT {
800 email: "info@example.com",
801 pass: crypto::argon2::generate("company-password"),
802 owner: employee:2,
803 };
804 CREATE employee:1 CONTENT {
805 email: "member@example.com",
806 pass: crypto::argon2::generate("member-password"),
807 };
808 CREATE employee:2 CONTENT {
809 email: "owner@example.com",
810 pass: crypto::argon2::generate("owner-password"),
811 };
812 "#,
813 &sess,
814 None,
815 )
816 .await
817 .unwrap();
818
819 let mut sess = Session {
821 ns: Some("test".to_string()),
822 db: Some("test".to_string()),
823 ..Default::default()
824 };
825 let mut vars: HashMap<&str, Value> = HashMap::new();
826 vars.insert("email", "info@example.com".into());
827 vars.insert("pass", "company-password".into());
828 let res = db_access(
829 &ds,
830 &mut sess,
831 "test".to_string(),
832 "test".to_string(),
833 "owner".to_string(),
834 vars.into(),
835 )
836 .await;
837
838 assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
839 assert_eq!(sess.ns, Some("test".to_string()));
840 assert_eq!(sess.db, Some("test".to_string()));
841 assert_eq!(sess.ac, Some("owner".to_string()));
842 assert!(sess.au.id().starts_with("employee:"));
843 assert!(sess.au.is_record());
844 assert_eq!(sess.au.level().ns(), Some("test"));
845 assert_eq!(sess.au.level().db(), Some("test"));
846 assert!(sess.au.level().id().unwrap().starts_with("employee:"));
847 assert!(!sess.au.has_role(Role::Viewer), "Auth user expected to not have Viewer role");
849 assert!(!sess.au.has_role(Role::Editor), "Auth user expected to not have Editor role");
850 assert!(!sess.au.has_role(Role::Owner), "Auth user expected to not have Owner role");
851 let exp = sess.exp.unwrap();
853 let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
855 let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
856 assert!(
857 exp > min_exp && exp < max_exp,
858 "Session expiration is expected to follow the defined duration"
859 );
860 }
861
862 {
864 let ds = Datastore::new("memory").await.unwrap();
865 let sess = Session::owner().with_ns("test").with_db("test");
866 ds.execute(
867 r#"
868 DEFINE ACCESS user ON DATABASE TYPE RECORD
869 SIGNUP (
870 CREATE type::thing('user', $id)
871 )
872 AUTHENTICATE {
873 -- Not just signin, this clause runs across signin, signup and authenticate, which makes it a nice place to centralize logic
874 IF !$auth.enabled {
875 THROW "This user is not enabled";
876 };
877
878 -- Always need to return the user id back, otherwise auth generically fails
879 RETURN $auth;
880 }
881 DURATION FOR SESSION 2h
882 ;
883 "#,
884 &sess,
885 None,
886 )
887 .await
888 .unwrap();
889
890 let mut sess = Session {
892 ns: Some("test".to_string()),
893 db: Some("test".to_string()),
894 ..Default::default()
895 };
896 let mut vars: HashMap<&str, Value> = HashMap::new();
897 vars.insert("id", 1.into());
898 let res = db_access(
899 &ds,
900 &mut sess,
901 "test".to_string(),
902 "test".to_string(),
903 "user".to_string(),
904 vars.into(),
905 )
906 .await;
907
908 match res {
909 Err(Error::Thrown(e)) if e == "This user is not enabled" => {} res => panic!(
911 "Expected authentication to failed due to user not being enabled, but instead received: {:?}",
912 res
913 ),
914 }
915 }
916
917 {
919 let ds = Datastore::new("memory").await.unwrap();
920 let sess = Session::owner().with_ns("test").with_db("test");
921 ds.execute(
922 r#"
923 DEFINE ACCESS user ON DATABASE TYPE RECORD
924 SIGNUP (
925 CREATE type::thing('user', $id)
926 )
927 AUTHENTICATE {}
928 DURATION FOR SESSION 2h
929 ;
930 "#,
931 &sess,
932 None,
933 )
934 .await
935 .unwrap();
936
937 let mut sess = Session {
939 ns: Some("test".to_string()),
940 db: Some("test".to_string()),
941 ..Default::default()
942 };
943 let mut vars: HashMap<&str, Value> = HashMap::new();
944 vars.insert("id", 1.into());
945 let res = db_access(
946 &ds,
947 &mut sess,
948 "test".to_string(),
949 "test".to_string(),
950 "user".to_string(),
951 vars.into(),
952 )
953 .await;
954
955 match res {
956 Err(Error::InvalidAuth) => {} res => panic!(
958 "Expected authentication to generally fail, but instead received: {:?}",
959 res
960 ),
961 }
962 }
963 }
964
965 #[tokio::test]
966 #[ignore = "flaky"]
967 async fn test_signup_record_transaction_conflict() {
968 {
970 let ds = Datastore::new("memory").await.unwrap();
971 let sess = Session::owner().with_ns("test").with_db("test");
972 ds.execute(
973 r#"
974 DEFINE ACCESS user ON DATABASE TYPE RECORD
975 SIGNIN (
976 SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
977 )
978 SIGNUP {
979 -- Concurrently write to the same document
980 UPSERT count:1 SET count += 1;
981 -- Increase the duration of the transaction
982 sleep(500ms);
983 -- Continue with authentication
984 RETURN (CREATE user CONTENT {
985 name: $user,
986 pass: crypto::argon2::generate($pass)
987 })
988 }
989 DURATION FOR SESSION 2h
990 ;
991 "#,
992 &sess,
993 None,
994 )
995 .await
996 .unwrap();
997
998 let mut sess1 = Session {
1000 ns: Some("test".to_string()),
1001 db: Some("test".to_string()),
1002 ..Default::default()
1003 };
1004 let mut sess2 = Session {
1005 ns: Some("test".to_string()),
1006 db: Some("test".to_string()),
1007 ..Default::default()
1008 };
1009 let mut vars: HashMap<&str, Value> = HashMap::new();
1010 vars.insert("user", "user".into());
1011 vars.insert("pass", "pass".into());
1012
1013 let (res1, res2) = tokio::join!(
1014 db_access(
1015 &ds,
1016 &mut sess1,
1017 "test".to_string(),
1018 "test".to_string(),
1019 "user".to_string(),
1020 vars.clone().into(),
1021 ),
1022 db_access(
1023 &ds,
1024 &mut sess2,
1025 "test".to_string(),
1026 "test".to_string(),
1027 "user".to_string(),
1028 vars.into(),
1029 )
1030 );
1031
1032 match (res1, res2) {
1033 (Ok(r1), Ok(r2)) => panic!("Expected authentication to fail in one instance, but instead received: {:?} and {:?}", r1, r2),
1034 (Err(e1), Err(e2)) => panic!("Expected authentication to fail in one instance, but instead received: {:?} and {:?}", e1, e2),
1035 (Err(e1), Ok(_)) => match &e1 {
1036 Error::UnexpectedAuth => {} e => panic!("Expected authentication to return an UnexpectedAuth error, but insted got: {e}")
1038 }
1039 (Ok(_), Err(e2)) => match &e2 {
1040 Error::UnexpectedAuth => {} e => panic!("Expected authentication to return an UnexpectedAuth error, but insted got: {e}")
1042 }
1043 }
1044 }
1045
1046 {
1048 let ds = Datastore::new("memory").await.unwrap();
1049 let sess = Session::owner().with_ns("test").with_db("test");
1050 ds.execute(
1051 r#"
1052 DEFINE ACCESS user ON DATABASE TYPE RECORD
1053 SIGNUP (
1054 CREATE type::thing('user', $id)
1055 )
1056 AUTHENTICATE {
1057 -- Concurrently write to the same document
1058 UPSERT count:1 SET count += 1;
1059 -- Increase the duration of the transaction
1060 sleep(500ms);
1061 -- Continue with authentication
1062 $auth.id
1063 }
1064 DURATION FOR SESSION 2h
1065 ;
1066 "#,
1067 &sess,
1068 None,
1069 )
1070 .await
1071 .unwrap();
1072
1073 let mut sess1 = Session {
1075 ns: Some("test".to_string()),
1076 db: Some("test".to_string()),
1077 ..Default::default()
1078 };
1079 let mut sess2 = Session {
1080 ns: Some("test".to_string()),
1081 db: Some("test".to_string()),
1082 ..Default::default()
1083 };
1084 let mut vars: HashMap<&str, Value> = HashMap::new();
1085 vars.insert("id", 1.into());
1086
1087 let (res1, res2) = tokio::join!(
1088 db_access(
1089 &ds,
1090 &mut sess1,
1091 "test".to_string(),
1092 "test".to_string(),
1093 "user".to_string(),
1094 vars.clone().into(),
1095 ),
1096 db_access(
1097 &ds,
1098 &mut sess2,
1099 "test".to_string(),
1100 "test".to_string(),
1101 "user".to_string(),
1102 vars.into(),
1103 )
1104 );
1105
1106 match (res1, res2) {
1107 (Ok(r1), Ok(r2)) => panic!("Expected authentication to fail in one instance, but instead received: {:?} and {:?}", r1, r2),
1108 (Err(e1), Err(e2)) => panic!("Expected authentication to fail in one instance, but instead received: {:?} and {:?}", e1, e2),
1109 (Err(e1), Ok(_)) => match &e1 {
1110 Error::UnexpectedAuth => {} e => panic!("Expected authentication to return an UnexpectedAuth error, but insted got: {e}")
1112 }
1113 (Ok(_), Err(e2)) => match &e2 {
1114 Error::UnexpectedAuth => {} e => panic!("Expected authentication to return an UnexpectedAuth error, but insted got: {e}")
1116 }
1117 }
1118 }
1119 }
1120}