surrealpatch/core/src/iam/signin.rs
Gerard Guillemas Martos b7583a653a
Merge commit from fork
* fix

* move check into iam

* refactor

* fix typo

* add test

* remove is_computed

* impl validate_computed for array

* range support

* clippy

* tidy

* tidy

* add comments

---------

Co-authored-by: Raphael Darley <raphael.darley@surrealdb.com>
2024-09-10 17:19:45 +02:00

3934 lines
117 KiB
Rust

use super::verify::{
authenticate_generic, authenticate_record, verify_db_creds, verify_ns_creds, verify_root_creds,
};
use super::{Actor, Level, Role};
use crate::cnf::{EXPERIMENTAL_BEARER_ACCESS, INSECURE_FORWARD_ACCESS_ERRORS, SERVER_NAME};
use crate::dbs::Session;
use crate::err::Error;
use crate::iam::issue::{config, expiration};
use crate::iam::token::{Claims, HEADER};
use crate::iam::Auth;
use crate::kvs::{Datastore, LockType::*, TransactionType::*};
use crate::sql::statements::{access, AccessGrant};
use crate::sql::AccessType;
use crate::sql::Datetime;
use crate::sql::Object;
use crate::sql::Value;
use chrono::Utc;
use jsonwebtoken::{encode, EncodingKey, Header};
use std::sync::Arc;
use subtle::ConstantTimeEq;
use uuid::Uuid;
pub async fn signin(kvs: &Datastore, session: &mut Session, vars: Object) -> Result<String, Error> {
// Check vars contains only computed values
vars.validate_computed()?;
// Parse the specified variables
let ns = vars.get("NS").or_else(|| vars.get("ns"));
let db = vars.get("DB").or_else(|| vars.get("db"));
let ac = vars.get("AC").or_else(|| vars.get("ac"));
// Check if the parameters exist
match (ns, db, ac) {
// DB signin with access method
(Some(ns), Some(db), Some(ac)) => {
// Process the provided values
let ns = ns.to_raw_string();
let db = db.to_raw_string();
let ac = ac.to_raw_string();
// Attempt to signin using specified access method
super::signin::db_access(kvs, session, ns, db, ac, vars).await
}
// DB signin with user credentials
(Some(ns), Some(db), None) => {
// Get the provided user and pass
let user = vars.get("user");
let pass = vars.get("pass");
// Validate the user and pass
match (user, pass) {
// There is a username and password
(Some(user), Some(pass)) => {
// Process the provided values
let ns = ns.to_raw_string();
let db = db.to_raw_string();
let user = user.to_raw_string();
let pass = pass.to_raw_string();
// Attempt to signin to database
super::signin::db_user(kvs, session, ns, db, user, pass).await
}
_ => Err(Error::MissingUserOrPass),
}
}
// NS signin with access method
(Some(ns), None, Some(ac)) => {
// Process the provided values
let ns = ns.to_raw_string();
let ac = ac.to_raw_string();
// Attempt to signin using specified access method
super::signin::ns_access(kvs, session, ns, ac, vars).await
}
// NS signin with user credentials
(Some(ns), None, None) => {
// Get the provided user and pass
let user = vars.get("user");
let pass = vars.get("pass");
// Validate the user and pass
match (user, pass) {
// There is a username and password
(Some(user), Some(pass)) => {
// Process the provided values
let ns = ns.to_raw_string();
let user = user.to_raw_string();
let pass = pass.to_raw_string();
// Attempt to signin to namespace
super::signin::ns_user(kvs, session, ns, user, pass).await
}
_ => Err(Error::MissingUserOrPass),
}
}
// ROOT signin with user credentials
(None, None, None) => {
// Get the provided user and pass
let user = vars.get("user");
let pass = vars.get("pass");
// Validate the user and pass
match (user, pass) {
// There is a username and password
(Some(user), Some(pass)) => {
// Process the provided values
let user = user.to_raw_string();
let pass = pass.to_raw_string();
// Attempt to signin to root
super::signin::root_user(kvs, session, user, pass).await
}
_ => Err(Error::MissingUserOrPass),
}
}
_ => Err(Error::NoSigninTarget),
}
}
pub async fn db_access(
kvs: &Datastore,
session: &mut Session,
ns: String,
db: String,
ac: String,
vars: Object,
) -> Result<String, Error> {
// Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?;
// Fetch the specified access method from storage
let access = tx.get_db_access(&ns, &db, &ac).await;
// Ensure that the transaction is cancelled
tx.cancel().await?;
// Check the provided access method exists
match access {
Ok(av) => {
// Check the access method type
// All access method types are supported except for JWT
// The JWT access method is the one that is internal to SurrealDB
// The equivalent of signing in with JWT is to authenticate it
match &av.kind {
AccessType::Record(at) => {
// Check if the record access method supports issuing tokens
let iss = match &at.jwt.issue {
Some(iss) => iss.clone(),
_ => return Err(Error::AccessMethodMismatch),
};
match &at.signin {
// This record access allows signin
Some(val) => {
// Setup the query params
let vars = Some(vars.0);
// Setup the system session for finding the signin record
let mut sess = Session::editor().with_ns(&ns).with_db(&db);
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
// Compute the value with the params
match kvs.evaluate(val, &sess, vars).await {
// The signin value succeeded
Ok(val) => {
match val.record() {
// There is a record returned
Some(mut rid) => {
// Create the authentication key
let key = config(iss.alg, &iss.key)?;
// Create the authentication claim
let claims = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: expiration(av.duration.token)?,
jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()),
db: Some(db.to_owned()),
ac: Some(ac.to_owned()),
id: Some(rid.to_raw()),
..Claims::default()
};
// AUTHENTICATE clause
if let Some(au) = &av.authenticate {
// Setup the system session for finding the signin record
let mut sess =
Session::editor().with_ns(&ns).with_db(&db);
sess.rd = Some(rid.clone().into());
sess.tk = Some((&claims).into());
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
rid = authenticate_record(kvs, &sess, au).await?;
}
// Log the authenticated access method info
trace!(
"Signing in to database with access method `{}`",
ac
);
// Create the authentication token
let enc =
encode(&Header::new(iss.alg.into()), &claims, &key);
// Set the authentication on the session
session.tk = Some((&claims).into());
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.ac = Some(ac.to_owned());
session.rd = Some(Value::from(rid.to_owned()));
session.exp = expiration(av.duration.session)?;
session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(),
Default::default(),
Level::Record(ns, db, rid.to_string()),
)));
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(tk),
_ => Err(Error::TokenMakingFailed),
}
}
_ => Err(Error::NoRecordFound),
}
}
Err(e) => match e {
Error::Thrown(_) => Err(e),
e if *INSECURE_FORWARD_ACCESS_ERRORS => Err(e),
_ => Err(Error::AccessRecordSigninQueryFailed),
},
}
}
_ => Err(Error::AccessRecordNoSignin),
}
}
AccessType::Bearer(at) => {
// TODO(gguillemas): Remove this once bearer access is no longer experimental.
if !*EXPERIMENTAL_BEARER_ACCESS {
// Return opaque error to avoid leaking the existence of the feature.
return Err(Error::InvalidAuth);
}
// Check if the bearer access method supports issuing tokens.
let iss = match &at.jwt.issue {
Some(iss) => iss.clone(),
_ => return Err(Error::AccessMethodMismatch),
};
// Extract key identifier and key from the provided variables.
let (kid, key) = validate_grant_bearer(vars)?;
// Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?;
// Fetch the specified access grant from storage
let gr = match tx.get_db_access_grant(&ns, &db, &ac, &kid).await {
Ok(gr) => gr,
// Return opaque error to avoid leaking existence of the grant.
_ => return Err(Error::InvalidAuth),
};
// Ensure that the transaction is cancelled.
tx.cancel().await?;
// Authenticate bearer key against stored grant.
verify_grant_bearer(&gr, key)?;
// If the subject of the grant is a system user, get their roles.
let roles = if let Some(access::Subject::User(user)) = &gr.subject {
// Create a new readonly transaction.
let tx = kvs.transaction(Read, Optimistic).await?;
// Fetch the specified user from storage.
let user = tx.get_db_user(&ns, &db, user).await.map_err(|e| {
trace!("Error while authenticating to database `{ns}/{db}`: {e}");
// Return opaque error to avoid leaking grant subject existence.
Error::InvalidAuth
})?;
// Ensure that the transaction is cancelled.
tx.cancel().await?;
user.roles.clone()
} else {
vec![]
};
// Create the authentication key.
let key = config(iss.alg, &iss.key)?;
// Create the authentication claim.
let claims = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: expiration(av.duration.token)?,
jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()),
db: Some(db.to_owned()),
ac: Some(ac.to_owned()),
id: match &gr.subject {
Some(access::Subject::User(user)) => Some(user.to_raw()),
Some(access::Subject::Record(rid)) => Some(rid.to_raw()),
// Return opaque error as this code should not be reachable.
None => return Err(Error::InvalidAuth),
},
roles: match &gr.subject {
Some(access::Subject::User(_)) => {
Some(roles.iter().map(|v| v.to_string()).collect())
}
Some(access::Subject::Record(_)) => Default::default(),
// Return opaque error as this code should not be reachable.
None => return Err(Error::InvalidAuth),
},
..Claims::default()
};
// AUTHENTICATE clause
if let Some(au) = &av.authenticate {
// Setup the system session for executing the clause
let mut sess = Session::editor().with_ns(&ns).with_db(&db);
sess.tk = Some((&claims).into());
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
authenticate_generic(kvs, &sess, au).await?;
}
// Log the authenticated access method information.
trace!("Signing in to database with bearer access method `{}`", ac);
// Create the authentication token.
let enc = encode(&Header::new(iss.alg.into()), &claims, &key);
// Set the authentication on the session.
session.tk = Some((&claims).into());
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.ac = Some(ac.to_owned());
session.exp = expiration(av.duration.session)?;
match &gr.subject {
Some(access::Subject::User(user)) => {
session.au = Arc::new(Auth::new(Actor::new(
user.to_string(),
roles.iter().map(Role::from).collect(),
Level::Database(ns, db),
)));
}
Some(access::Subject::Record(rid)) => {
session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(),
Default::default(),
Level::Record(ns, db, rid.to_string()),
)));
session.rd = Some(Value::from(rid.to_owned()));
}
// Return opaque error as this code should not be reachable.
None => return Err(Error::InvalidAuth),
};
// Check the authentication token.
match enc {
// The authentication token was created successfully.
Ok(tk) => Ok(tk),
_ => Err(Error::TokenMakingFailed),
}
}
_ => Err(Error::AccessMethodMismatch),
}
}
_ => Err(Error::AccessNotFound),
}
}
pub async fn db_user(
kvs: &Datastore,
session: &mut Session,
ns: String,
db: String,
user: String,
pass: String,
) -> Result<String, Error> {
match verify_db_creds(kvs, &ns, &db, &user, &pass).await {
Ok(u) => {
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: expiration(u.duration.token)?,
jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()),
db: Some(db.to_owned()),
id: Some(user),
..Claims::default()
};
// Log the authenticated database info
trace!("Signing in to database `{}`", db);
// Create the authentication token
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some((&val).into());
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.exp = expiration(u.duration.session)?;
session.au = Arc::new((&u, Level::Database(ns.to_owned(), db.to_owned())).into());
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(tk),
_ => Err(Error::TokenMakingFailed),
}
}
_ => Err(Error::InvalidAuth),
}
}
pub async fn ns_access(
kvs: &Datastore,
session: &mut Session,
ns: String,
ac: String,
vars: Object,
) -> Result<String, Error> {
// Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?;
// Fetch the specified access method from storage
let access = tx.get_ns_access(&ns, &ac).await;
// Ensure that the transaction is cancelled
tx.cancel().await?;
// Check the provided access method exists
match access {
Ok(av) => {
// Check the access method type
match &av.kind {
AccessType::Bearer(at) => {
// TODO(gguillemas): Remove this once bearer access is no longer experimental.
if !*EXPERIMENTAL_BEARER_ACCESS {
// Return opaque error to avoid leaking the existence of the feature.
return Err(Error::InvalidAuth);
}
// Check if the bearer access method supports issuing tokens.
let iss = match &at.jwt.issue {
Some(iss) => iss.clone(),
_ => return Err(Error::AccessMethodMismatch),
};
// Extract key identifier and key from the provided variables.
let (kid, key) = validate_grant_bearer(vars)?;
// Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?;
// Fetch the specified access grant from storage
let gr = match tx.get_ns_access_grant(&ns, &ac, &kid).await {
Ok(gr) => gr,
// Return opaque error to avoid leaking existence of the grant.
_ => return Err(Error::InvalidAuth),
};
// Ensure that the transaction is cancelled.
tx.cancel().await?;
// Authenticate bearer key against stored grant.
verify_grant_bearer(&gr, key)?;
// If the subject of the grant is a system user, get their roles.
let roles = if let Some(access::Subject::User(user)) = &gr.subject {
// Create a new readonly transaction.
let tx = kvs.transaction(Read, Optimistic).await?;
// Fetch the specified user from storage.
let user = tx.get_ns_user(&ns, user).await.map_err(|e| {
trace!("Error while authenticating to namespace `{ns}`: {e}");
// Return opaque error to avoid leaking grant subject existence.
Error::InvalidAuth
})?;
// Ensure that the transaction is cancelled.
tx.cancel().await?;
user.roles.clone()
} else {
vec![]
};
// Create the authentication key.
let key = config(iss.alg, &iss.key)?;
// Create the authentication claim.
let claims = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: expiration(av.duration.token)?,
jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()),
ac: Some(ac.to_owned()),
id: match &gr.subject {
Some(access::Subject::User(user)) => Some(user.to_raw()),
// Return opaque error as this code should not be reachable.
_ => return Err(Error::InvalidAuth),
},
roles: match &gr.subject {
Some(access::Subject::User(_)) => {
Some(roles.iter().map(|v| v.to_string()).collect())
}
// Return opaque error as this code should not be reachable.
_ => return Err(Error::InvalidAuth),
},
..Claims::default()
};
// AUTHENTICATE clause
if let Some(au) = &av.authenticate {
// Setup the system session for executing the clause
let mut sess = Session::editor().with_ns(&ns);
sess.tk = Some((&claims).into());
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
authenticate_generic(kvs, &sess, au).await?;
}
// Log the authenticated access method information.
trace!("Signing in to database with bearer access method `{}`", ac);
// Create the authentication token.
let enc = encode(&Header::new(iss.alg.into()), &claims, &key);
// Set the authentication on the session.
session.tk = Some((&claims).into());
session.ns = Some(ns.to_owned());
session.ac = Some(ac.to_owned());
session.exp = expiration(av.duration.session)?;
match &gr.subject {
Some(access::Subject::User(user)) => {
session.au = Arc::new(Auth::new(Actor::new(
user.to_string(),
roles.iter().map(Role::from).collect(),
Level::Namespace(ns),
)));
}
// Return opaque error as this code should not be reachable.
_ => return Err(Error::InvalidAuth),
};
// Check the authentication token.
match enc {
// The authentication token was created successfully.
Ok(tk) => Ok(tk),
_ => Err(Error::TokenMakingFailed),
}
}
_ => Err(Error::AccessMethodMismatch),
}
}
_ => Err(Error::AccessNotFound),
}
}
pub async fn ns_user(
kvs: &Datastore,
session: &mut Session,
ns: String,
user: String,
pass: String,
) -> Result<String, Error> {
match verify_ns_creds(kvs, &ns, &user, &pass).await {
Ok(u) => {
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: expiration(u.duration.token)?,
jti: Some(Uuid::new_v4().to_string()),
ns: Some(ns.to_owned()),
id: Some(user),
..Claims::default()
};
// Log the authenticated namespace info
trace!("Signing in to namespace `{}`", ns);
// Create the authentication token
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some((&val).into());
session.ns = Some(ns.to_owned());
session.exp = expiration(u.duration.session)?;
session.au = Arc::new((&u, Level::Namespace(ns.to_owned())).into());
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(tk),
_ => Err(Error::TokenMakingFailed),
}
}
// The password did not verify
_ => Err(Error::InvalidAuth),
}
}
pub async fn root_user(
kvs: &Datastore,
session: &mut Session,
user: String,
pass: String,
) -> Result<String, Error> {
match verify_root_creds(kvs, &user, &pass).await {
Ok(u) => {
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: expiration(u.duration.token)?,
jti: Some(Uuid::new_v4().to_string()),
id: Some(user),
..Claims::default()
};
// Log the authenticated root info
trace!("Signing in as root");
// Create the authentication token
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
session.exp = expiration(u.duration.session)?;
session.au = Arc::new((&u, Level::Root).into());
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(tk),
_ => Err(Error::TokenMakingFailed),
}
}
// The password did not verify
_ => Err(Error::InvalidAuth),
}
}
pub async fn root_access(
kvs: &Datastore,
session: &mut Session,
ac: String,
vars: Object,
) -> Result<String, Error> {
// Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?;
// Fetch the specified access method from storage
let access = tx.get_root_access(&ac).await;
// Ensure that the transaction is cancelled
tx.cancel().await?;
// Check the provided access method exists
match access {
Ok(av) => {
// Check the access method type
match &av.kind {
AccessType::Bearer(at) => {
// TODO(gguillemas): Remove this once bearer access is no longer experimental.
if !*EXPERIMENTAL_BEARER_ACCESS {
// Return opaque error to avoid leaking the existence of the feature.
return Err(Error::InvalidAuth);
}
// Check if the bearer access method supports issuing tokens.
let iss = match &at.jwt.issue {
Some(iss) => iss.clone(),
_ => return Err(Error::AccessMethodMismatch),
};
// Extract key identifier and key from the provided variables.
let (kid, key) = validate_grant_bearer(vars)?;
// Create a new readonly transaction
let tx = kvs.transaction(Read, Optimistic).await?;
// Fetch the specified access grant from storage
let gr = match tx.get_root_access_grant(&ac, &kid).await {
Ok(gr) => gr,
// Return opaque error to avoid leaking existence of the grant.
_ => return Err(Error::InvalidAuth),
};
// Ensure that the transaction is cancelled.
tx.cancel().await?;
// Authenticate bearer key against stored grant.
verify_grant_bearer(&gr, key)?;
// If the subject of the grant is a system user, get their roles.
let roles = if let Some(access::Subject::User(user)) = &gr.subject {
// Create a new readonly transaction.
let tx = kvs.transaction(Read, Optimistic).await?;
// Fetch the specified user from storage.
let user = tx.get_root_user(user).await.map_err(|e| {
trace!("Error while authenticating to root: {e}");
// Return opaque error to avoid leaking grant subject existence.
Error::InvalidAuth
})?;
// Ensure that the transaction is cancelled.
tx.cancel().await?;
user.roles.clone()
} else {
vec![]
};
// Create the authentication key.
let key = config(iss.alg, &iss.key)?;
// Create the authentication claim.
let claims = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: expiration(av.duration.token)?,
jti: Some(Uuid::new_v4().to_string()),
ac: Some(ac.to_owned()),
id: match &gr.subject {
Some(access::Subject::User(user)) => Some(user.to_raw()),
// Return opaque error as this code should not be reachable.
_ => return Err(Error::InvalidAuth),
},
roles: match &gr.subject {
Some(access::Subject::User(_)) => {
Some(roles.iter().map(|v| v.to_string()).collect())
}
// Return opaque error as this code should not be reachable.
_ => return Err(Error::InvalidAuth),
},
..Claims::default()
};
// AUTHENTICATE clause
if let Some(au) = &av.authenticate {
// Setup the system session for executing the clause
let mut sess = Session::editor();
sess.tk = Some((&claims).into());
sess.ip.clone_from(&session.ip);
sess.or.clone_from(&session.or);
authenticate_generic(kvs, &sess, au).await?;
}
// Log the authenticated access method information.
trace!("Signing in to database with bearer access method `{}`", ac);
// Create the authentication token.
let enc = encode(&Header::new(iss.alg.into()), &claims, &key);
// Set the authentication on the session.
session.tk = Some(claims.into());
session.ac = Some(ac.to_owned());
session.exp = expiration(av.duration.session)?;
match &gr.subject {
Some(access::Subject::User(user)) => {
session.au = Arc::new(Auth::new(Actor::new(
user.to_string(),
roles.iter().map(Role::from).collect(),
Level::Root,
)));
}
// Return opaque error as this code should not be reachable.
_ => return Err(Error::InvalidAuth),
};
// Check the authentication token.
match enc {
// The authentication token was created successfully.
Ok(tk) => Ok(tk),
_ => Err(Error::TokenMakingFailed),
}
}
_ => Err(Error::AccessMethodMismatch),
}
}
_ => Err(Error::AccessNotFound),
}
}
pub fn validate_grant_bearer(vars: Object) -> Result<(String, String), Error> {
// Extract the provided key.
let key = match vars.get("key") {
Some(key) => key.to_raw_string(),
None => return Err(Error::AccessBearerMissingKey),
};
if key.len() != access::GRANT_BEARER_LENGTH {
return Err(Error::AccessGrantBearerInvalid);
}
// Retrieve the prefix from the provided key.
let prefix: String = key.chars().take(access::GRANT_BEARER_PREFIX.len()).collect();
// Check the length of the key prefix.
if prefix != access::GRANT_BEARER_PREFIX {
return Err(Error::AccessGrantBearerInvalid);
}
// Retrieve the key identifier from the provided key.
let kid: String = key
.chars()
.skip(access::GRANT_BEARER_PREFIX.len() + 1)
.take(access::GRANT_BEARER_ID_LENGTH)
.collect();
// Check the length of the key identifier.
if kid.len() != access::GRANT_BEARER_ID_LENGTH {
return Err(Error::AccessGrantBearerInvalid);
};
Ok((kid, key))
}
pub fn verify_grant_bearer(gr: &Arc<AccessGrant>, key: String) -> Result<(), Error> {
// Check if the grant is revoked or expired.
match (&gr.expiration, &gr.revocation) {
(None, None) => {}
(Some(exp), None) => {
if exp < &Datetime::default() {
// Return opaque error to avoid leaking revocation status.
return Err(Error::InvalidAuth);
}
}
_ => return Err(Error::InvalidAuth),
}
// Check if the provided key matches the bearer key in the grant.
// We use time-constant comparison to prevent timing attacks.
if let access::Grant::Bearer(grant) = &gr.grant {
let grant_key_bytes: &[u8] = grant.key.as_bytes();
let signin_key_bytes: &[u8] = key.as_bytes();
let ok: bool = grant_key_bytes.ct_eq(signin_key_bytes).into();
if !ok {
return Err(Error::InvalidAuth);
}
};
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::iam::Role;
use chrono::Duration;
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use std::collections::HashMap;
#[tokio::test]
async fn test_signin_record() {
// Test with correct credentials
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute(
r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
)
SIGNUP (
CREATE user CONTENT {
name: $user,
pass: crypto::argon2::generate($pass)
}
)
DURATION FOR SESSION 2h
;
CREATE user:test CONTENT {
name: 'user',
pass: crypto::argon2::generate('pass')
}
"#,
&sess,
None,
)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("user", "user".into());
vars.insert("pass", "pass".into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
vars.into(),
)
.await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.au.id(), "user:test");
assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
assert_eq!(sess.au.level().id(), Some("user:test"));
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Expiration should match the defined duration
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to follow the defined duration"
);
}
// Test with incorrect credentials
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute(
r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
)
SIGNUP (
CREATE user CONTENT {
name: $user,
pass: crypto::argon2::generate($pass)
}
)
DURATION FOR SESSION 2h
;
CREATE user:test CONTENT {
name: 'user',
pass: crypto::argon2::generate('pass')
}
"#,
&sess,
None,
)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("user", "user".into());
vars.insert("pass", "incorrect".into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
vars.into(),
)
.await;
assert!(res.is_err(), "Unexpected successful signin: {:?}", res);
}
}
#[tokio::test]
async fn test_signin_record_with_jwt_issuer() {
// Test with correct credentials
{
let public_key = r#"-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu1SU1LfVLPHCozMxH2Mo
4lgOEePzNm0tRgeLezV6ffAt0gunVTLw7onLRnrq0/IzW7yWR7QkrmBL7jTKEn5u
+qKhbwKfBstIs+bMY2Zkp18gnTxKLxoS2tFczGkPLPgizskuemMghRniWaoLcyeh
kd3qqGElvW/VDL5AaWTg0nLVkjRo9z+40RQzuVaE8AkAFmxZzow3x+VJYKdjykkJ
0iT9wCS0DRTXu269V264Vf/3jvredZiKRkgwlL9xNAwxXFg0x/XFw005UWVRIkdg
cKWTjpBP2dPwVZ4WWC+9aGVd+Gyn1o0CLelf4rEjGoXbAAEgAqeGUxrcIlbjXfbc
mwIDAQAB
-----END PUBLIC KEY-----"#;
let private_key = r#"-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC7VJTUt9Us8cKj
MzEfYyjiWA4R4/M2bS1GB4t7NXp98C3SC6dVMvDuictGeurT8jNbvJZHtCSuYEvu
NMoSfm76oqFvAp8Gy0iz5sxjZmSnXyCdPEovGhLa0VzMaQ8s+CLOyS56YyCFGeJZ
qgtzJ6GR3eqoYSW9b9UMvkBpZODSctWSNGj3P7jRFDO5VoTwCQAWbFnOjDfH5Ulg
p2PKSQnSJP3AJLQNFNe7br1XbrhV//eO+t51mIpGSDCUv3E0DDFcWDTH9cXDTTlR
ZVEiR2BwpZOOkE/Z0/BVnhZYL71oZV34bKfWjQIt6V/isSMahdsAASACp4ZTGtwi
VuNd9tybAgMBAAECggEBAKTmjaS6tkK8BlPXClTQ2vpz/N6uxDeS35mXpqasqskV
laAidgg/sWqpjXDbXr93otIMLlWsM+X0CqMDgSXKejLS2jx4GDjI1ZTXg++0AMJ8
sJ74pWzVDOfmCEQ/7wXs3+cbnXhKriO8Z036q92Qc1+N87SI38nkGa0ABH9CN83H
mQqt4fB7UdHzuIRe/me2PGhIq5ZBzj6h3BpoPGzEP+x3l9YmK8t/1cN0pqI+dQwY
dgfGjackLu/2qH80MCF7IyQaseZUOJyKrCLtSD/Iixv/hzDEUPfOCjFDgTpzf3cw
ta8+oE4wHCo1iI1/4TlPkwmXx4qSXtmw4aQPz7IDQvECgYEA8KNThCO2gsC2I9PQ
DM/8Cw0O983WCDY+oi+7JPiNAJwv5DYBqEZB1QYdj06YD16XlC/HAZMsMku1na2T
N0driwenQQWzoev3g2S7gRDoS/FCJSI3jJ+kjgtaA7Qmzlgk1TxODN+G1H91HW7t
0l7VnL27IWyYo2qRRK3jzxqUiPUCgYEAx0oQs2reBQGMVZnApD1jeq7n4MvNLcPv
t8b/eU9iUv6Y4Mj0Suo/AU8lYZXm8ubbqAlwz2VSVunD2tOplHyMUrtCtObAfVDU
AhCndKaA9gApgfb3xw1IKbuQ1u4IF1FJl3VtumfQn//LiH1B3rXhcdyo3/vIttEk
48RakUKClU8CgYEAzV7W3COOlDDcQd935DdtKBFRAPRPAlspQUnzMi5eSHMD/ISL
DY5IiQHbIH83D4bvXq0X7qQoSBSNP7Dvv3HYuqMhf0DaegrlBuJllFVVq9qPVRnK
xt1Il2HgxOBvbhOT+9in1BzA+YJ99UzC85O0Qz06A+CmtHEy4aZ2kj5hHjECgYEA
mNS4+A8Fkss8Js1RieK2LniBxMgmYml3pfVLKGnzmng7H2+cwPLhPIzIuwytXywh
2bzbsYEfYx3EoEVgMEpPhoarQnYPukrJO4gwE2o5Te6T5mJSZGlQJQj9q4ZB2Dfz
et6INsK0oG8XVGXSpQvQh3RUYekCZQkBBFcpqWpbIEsCgYAnM3DQf3FJoSnXaMhr
VBIovic5l0xFkEHskAjFTevO86Fsz1C2aSeRKSqGFoOQ0tmJzBEs1R6KqnHInicD
TQrKhArgLXX4v3CddjfTRJkFWDbE/CkvKZNOrcf1nhaGCPspRJj2KUkj1Fhl9Cnc
dn/RsYEONbwQSjIfMPkvxF+8HQ==
-----END PRIVATE KEY-----"#;
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute(
&format!(
r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN (
SELECT * FROM user WHERE name = $user AND crypto::argon2::compare(pass, $pass)
)
SIGNUP (
CREATE user CONTENT {{
name: $user,
pass: crypto::argon2::generate($pass)
}}
)
WITH JWT ALGORITHM RS256 KEY '{public_key}'
WITH ISSUER KEY '{private_key}'
DURATION FOR SESSION 2h, FOR TOKEN 15m
;
CREATE user:test CONTENT {{
name: 'user',
pass: crypto::argon2::generate('pass')
}}
"#
),
&sess,
None,
)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("user", "user".into());
vars.insert("pass", "pass".into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
vars.into(),
)
.await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.au.id(), "user:test");
assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
assert_eq!(sess.au.level().id(), Some("user:test"));
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Session expiration should match the defined duration
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_sess_exp =
(Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_sess_exp =
(Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!(
exp > min_sess_exp && exp < max_sess_exp,
"Session expiration is expected to follow the defined duration"
);
// Decode token and check that it has been issued as intended
if let Ok(tk) = res {
// Check that token can be verified with the defined algorithm
let val = Validation::new(Algorithm::RS256);
// Check that token can be verified with the defined public key
let token_data = decode::<Claims>(
&tk,
&DecodingKey::from_rsa_pem(public_key.as_ref()).unwrap(),
&val,
)
.unwrap();
// Check that token has been issued with the defined algorithm
assert_eq!(token_data.header.alg, Algorithm::RS256);
// Check that token expiration matches the defined duration
// Expiration should match the current time plus token duration with some margin
let exp = match token_data.claims.exp {
Some(exp) => exp,
_ => panic!("Token is missing expiration claim"),
};
let min_tk_exp =
(Utc::now() + Duration::minutes(15) - Duration::seconds(10)).timestamp();
let max_tk_exp =
(Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
assert!(
exp > min_tk_exp && exp < max_tk_exp,
"Token expiration is expected to follow the defined duration"
);
// Check required token claims
assert_eq!(token_data.claims.ns, Some("test".to_string()));
assert_eq!(token_data.claims.db, Some("test".to_string()));
assert_eq!(token_data.claims.id, Some("user:test".to_string()));
assert_eq!(token_data.claims.ac, Some("user".to_string()));
} else {
panic!("Token could not be extracted from result")
}
}
}
#[tokio::test]
async fn test_signin_db_user() {
//
// Test without roles or expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute("DEFINE USER user ON DB PASSWORD 'pass'", &sess, None).await.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let res = db_user(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
"pass".to_string(),
)
.await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_db());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
}
//
// Test without roles and session expiration disabled
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute(
"DEFINE USER user ON DB PASSWORD 'pass' DURATION FOR TOKEN 365d, FOR SESSION NONE",
&sess,
None,
)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
db: Some("test".to_string()),
ns: Some("test".to_string()),
..Default::default()
};
let res = db_user(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
"pass".to_string(),
)
.await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_db());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, None, "Session expiration is expected to match defined duration");
// Decode token and check that it has been issued as intended
if let Ok(tk) = res {
// Decode token without validation
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.insecure_disable_signature_validation();
validation.validate_nbf = false;
validation.validate_exp = false;
validation
})
.unwrap();
// Check that token expiration matches the defined duration
// Expiration should match the current time plus token duration with some margin
let exp = match token_data.claims.exp {
Some(exp) => exp,
_ => panic!("Token is missing expiration claim"),
};
let min_tk_exp =
(Utc::now() + Duration::days(365) - Duration::seconds(10)).timestamp();
let max_tk_exp =
(Utc::now() + Duration::days(365) + Duration::seconds(10)).timestamp();
assert!(
exp > min_tk_exp && exp < max_tk_exp,
"Token expiration is expected to follow the defined duration"
);
// Check required token claims
assert_eq!(token_data.claims.ns, Some("test".to_string()));
assert_eq!(token_data.claims.db, Some("test".to_string()));
assert_eq!(token_data.claims.id, Some("user".to_string()));
} else {
panic!("Token could not be extracted from result")
}
}
//
// Test with roles and expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute("DEFINE USER user ON DB PASSWORD 'pass' ROLES EDITOR, OWNER DURATION FOR TOKEN 15m, FOR SESSION 6h", &sess, None)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let res = db_user(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
"pass".to_string(),
)
.await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_db());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
// Expiration has been set explicitly
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(6) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(6) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to match the defined duration"
);
// Decode token and check that it has been issued as intended
if let Ok(tk) = res {
// Decode token without validation
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.insecure_disable_signature_validation();
validation.validate_nbf = false;
validation.validate_exp = false;
validation
})
.unwrap();
// Check that token expiration matches the defined duration
// Expiration should match the current time plus token duration with some margin
let exp = match token_data.claims.exp {
Some(exp) => exp,
_ => panic!("Token is missing expiration claim"),
};
let min_tk_exp =
(Utc::now() + Duration::minutes(15) - Duration::seconds(10)).timestamp();
let max_tk_exp =
(Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
assert!(
exp > min_tk_exp && exp < max_tk_exp,
"Token expiration is expected to follow the defined duration"
);
// Check required token claims
assert_eq!(token_data.claims.ns, Some("test".to_string()));
assert_eq!(token_data.claims.db, Some("test".to_string()));
assert_eq!(token_data.claims.id, Some("user".to_string()));
} else {
panic!("Token could not be extracted from result")
}
}
// Test invalid password
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute("DEFINE USER user ON DB PASSWORD 'pass'", &sess, None).await.unwrap();
let mut sess = Session {
..Default::default()
};
let res = db_user(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
"invalid".to_string(),
)
.await;
assert!(res.is_err(), "Unexpected successful signin: {:?}", res);
}
}
#[tokio::test]
async fn test_signin_ns_user() {
//
// Test without roles or expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
ds.execute("DEFINE USER user ON NS PASSWORD 'pass'", &sess, None).await.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let res =
ns_user(&ds, &mut sess, "test".to_string(), "user".to_string(), "pass".to_string())
.await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_ns());
assert_eq!(sess.au.level().ns(), Some("test"));
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
}
//
// Test without roles and session expiration disabled
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
ds.execute(
"DEFINE USER user ON NS PASSWORD 'pass' DURATION FOR TOKEN 365d, FOR SESSION NONE",
&sess,
None,
)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let res =
ns_user(&ds, &mut sess, "test".to_string(), "user".to_string(), "pass".to_string())
.await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_ns());
assert_eq!(sess.au.level().ns(), Some("test"));
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, None, "Session expiration is expected to match defined duration");
// Decode token and check that it has been issued as intended
if let Ok(tk) = res {
// Decode token without validation
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.insecure_disable_signature_validation();
validation.validate_nbf = false;
validation.validate_exp = false;
validation
})
.unwrap();
// Check that token expiration matches the defined duration
// Expiration should match the current time plus token duration with some margin
let exp = match token_data.claims.exp {
Some(exp) => exp,
_ => panic!("Token is missing expiration claim"),
};
let min_tk_exp =
(Utc::now() + Duration::days(365) - Duration::seconds(10)).timestamp();
let max_tk_exp =
(Utc::now() + Duration::days(365) + Duration::seconds(10)).timestamp();
assert!(
exp > min_tk_exp && exp < max_tk_exp,
"Token expiration is expected to follow the defined duration"
);
// Check required token claims
assert_eq!(token_data.claims.ns, Some("test".to_string()));
assert_eq!(token_data.claims.db, None);
assert_eq!(token_data.claims.id, Some("user".to_string()));
} else {
panic!("Token could not be extracted from result")
}
}
//
// Test with roles and expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
ds.execute("DEFINE USER user ON NS PASSWORD 'pass' ROLES EDITOR, OWNER DURATION FOR TOKEN 15m, FOR SESSION 6h", &sess, None)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let res =
ns_user(&ds, &mut sess, "test".to_string(), "user".to_string(), "pass".to_string())
.await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_ns());
assert_eq!(sess.au.level().ns(), Some("test"));
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
// Expiration has been set explicitly
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(6) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(6) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to match the defined duration"
);
// Decode token and check that it has been issued as intended
if let Ok(tk) = res {
// Decode token without validation
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.insecure_disable_signature_validation();
validation.validate_nbf = false;
validation.validate_exp = false;
validation
})
.unwrap();
// Check that token expiration matches the defined duration
// Expiration should match the current time plus token duration with some margin
let exp = match token_data.claims.exp {
Some(exp) => exp,
_ => panic!("Token is missing expiration claim"),
};
let min_tk_exp =
(Utc::now() + Duration::minutes(15) - Duration::seconds(10)).timestamp();
let max_tk_exp =
(Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
assert!(
exp > min_tk_exp && exp < max_tk_exp,
"Token expiration is expected to follow the defined duration"
);
// Check required token claims
assert_eq!(token_data.claims.ns, Some("test".to_string()));
assert_eq!(token_data.claims.db, None);
assert_eq!(token_data.claims.id, Some("user".to_string()));
} else {
panic!("Token could not be extracted from result")
}
}
// Test invalid password
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
ds.execute("DEFINE USER user ON NS PASSWORD 'pass'", &sess, None).await.unwrap();
let mut sess = Session {
..Default::default()
};
let res = ns_user(
&ds,
&mut sess,
"test".to_string(),
"user".to_string(),
"invalid".to_string(),
)
.await;
assert!(res.is_err(), "Unexpected successful signin: {:?}", res);
}
}
#[tokio::test]
async fn test_signin_root_user() {
//
// Test without roles or expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
ds.execute("DEFINE USER user ON ROOT PASSWORD 'pass'", &sess, None).await.unwrap();
// Signin with the user
let mut sess = Session {
..Default::default()
};
let res = root_user(&ds, &mut sess, "user".to_string(), "pass".to_string()).await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_root());
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
}
//
// Test without roles and session expiration disabled
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
ds.execute("DEFINE USER user ON ROOT PASSWORD 'pass' DURATION FOR TOKEN 365d, FOR SESSION NONE", &sess, None).await.unwrap();
// Signin with the user
let mut sess = Session {
..Default::default()
};
let res = root_user(&ds, &mut sess, "user".to_string(), "pass".to_string()).await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_root());
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
assert_eq!(sess.exp, None, "Session expiration is expected to match defined duration");
// Decode token and check that it has been issued as intended
if let Ok(tk) = res {
// Decode token without validation
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.insecure_disable_signature_validation();
validation.validate_nbf = false;
validation.validate_exp = false;
validation
})
.unwrap();
// Check that token expiration matches the defined duration
// Expiration should match the current time plus token duration with some margin
let exp = match token_data.claims.exp {
Some(exp) => exp,
_ => panic!("Token is missing expiration claim"),
};
let min_tk_exp =
(Utc::now() + Duration::days(365) - Duration::seconds(10)).timestamp();
let max_tk_exp =
(Utc::now() + Duration::days(365) + Duration::seconds(10)).timestamp();
assert!(
exp > min_tk_exp && exp < max_tk_exp,
"Token expiration is expected to follow the defined duration"
);
// Check required token claims
assert_eq!(token_data.claims.ns, None);
assert_eq!(token_data.claims.db, None);
assert_eq!(token_data.claims.id, Some("user".to_string()));
} else {
panic!("Token could not be extracted from result")
}
}
//
// Test with roles and expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
ds.execute("DEFINE USER user ON ROOT PASSWORD 'pass' ROLES EDITOR, OWNER DURATION FOR TOKEN 15m, FOR SESSION 6h", &sess, None)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
..Default::default()
};
let res = root_user(&ds, &mut sess, "user".to_string(), "pass".to_string()).await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_root());
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
// Expiration has been set explicitly
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(6) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(6) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to match the defined duration"
);
// Decode token and check that it has been issued as intended
if let Ok(tk) = res {
// Decode token without validation
let token_data = decode::<Claims>(&tk, &DecodingKey::from_secret(&[]), &{
let mut validation = Validation::new(jsonwebtoken::Algorithm::HS256);
validation.insecure_disable_signature_validation();
validation.validate_nbf = false;
validation.validate_exp = false;
validation
})
.unwrap();
// Check that token expiration matches the defined duration
// Expiration should match the current time plus token duration with some margin
let exp = match token_data.claims.exp {
Some(exp) => exp,
_ => panic!("Token is missing expiration claim"),
};
let min_tk_exp =
(Utc::now() + Duration::minutes(15) - Duration::seconds(10)).timestamp();
let max_tk_exp =
(Utc::now() + Duration::minutes(15) + Duration::seconds(10)).timestamp();
assert!(
exp > min_tk_exp && exp < max_tk_exp,
"Token expiration is expected to follow the defined duration"
);
// Check required token claims
assert_eq!(token_data.claims.ns, None);
assert_eq!(token_data.claims.db, None);
assert_eq!(token_data.claims.id, Some("user".to_string()));
} else {
panic!("Token could not be extracted from result")
}
}
// Test invalid password
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
ds.execute("DEFINE USER user ON ROOT PASSWORD 'pass'", &sess, None).await.unwrap();
let mut sess = Session {
..Default::default()
};
let res = root_user(&ds, &mut sess, "user".to_string(), "invalid".to_string()).await;
assert!(res.is_err(), "Unexpected successful signin: {:?}", res);
}
}
#[tokio::test]
async fn test_signin_record_and_authenticate_clause() {
// Test with correct credentials
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute(
r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN (
SELECT * FROM type::thing('user', $id)
)
AUTHENTICATE (
-- Simple example increasing the record identifier by one
SELECT * FROM type::thing('user', record::id($auth) + 1)
)
DURATION FOR SESSION 2h
;
CREATE user:1, user:2;
"#,
&sess,
None,
)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("id", 1.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
vars.into(),
)
.await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.ac, Some("user".to_string()));
assert_eq!(sess.au.id(), "user:2");
assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
assert_eq!(sess.au.level().id(), Some("user:2"));
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Expiration should match the defined duration
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to follow the defined duration"
);
}
// Test with correct credentials and "realistic" scenario
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute(
r#"
DEFINE ACCESS owner ON DATABASE TYPE RECORD
SIGNUP (
-- Allow anyone to sign up as a new company
-- This automatically creates an owner with the same credentials
CREATE company CONTENT {
email: $email,
pass: crypto::argon2::generate($pass),
owner: (CREATE employee CONTENT {
email: $email,
pass: $pass,
}),
}
)
SIGNIN (
-- Allow company owners to log in directly with the company account
SELECT * FROM company WHERE email = $email AND crypto::argon2::compare(pass, $pass)
)
AUTHENTICATE (
-- If logging in with a company account, the session will be authenticated as the first owner
IF record::tb($auth) = "company" {
RETURN SELECT VALUE owner FROM company WHERE id = $auth
}
)
DURATION FOR SESSION 2h
;
CREATE company:1 CONTENT {
email: "info@example.com",
pass: crypto::argon2::generate("company-password"),
owner: employee:2,
};
CREATE employee:1 CONTENT {
email: "member@example.com",
pass: crypto::argon2::generate("member-password"),
};
CREATE employee:2 CONTENT {
email: "owner@example.com",
pass: crypto::argon2::generate("owner-password"),
};
"#,
&sess,
None,
)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("email", "info@example.com".into());
vars.insert("pass", "company-password".into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"owner".to_string(),
vars.into(),
)
.await;
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.ac, Some("owner".to_string()));
assert_eq!(sess.au.id(), "employee:2");
assert!(sess.au.is_record());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
assert_eq!(sess.au.level().id(), Some("employee:2"));
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Expiration should match the defined duration
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to follow the defined duration"
);
}
// Test being able to fail authentication
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute(
r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN (
SELECT * FROM type::thing('user', $id)
)
AUTHENTICATE {
-- Not just signin, this clause runs across signin, signup and authenticate, which makes it a nice place to centralize logic
IF !$auth.enabled {
THROW "This user is not enabled";
};
-- Always need to return the user id back, otherwise auth generically fails
RETURN $auth;
}
DURATION FOR SESSION 2h
;
CREATE user:1 SET enabled = false;
"#,
&sess,
None,
)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("id", 1.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
vars.into(),
)
.await;
match res {
Err(Error::Thrown(e)) if e == "This user is not enabled" => {} // ok
res => panic!(
"Expected authentication to failed due to user not being enabled, but instead received: {:?}",
res
),
}
}
// Test AUTHENTICATE clause not returning a value
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute(
r#"
DEFINE ACCESS user ON DATABASE TYPE RECORD
SIGNIN (
SELECT * FROM type::thing('user', $id)
)
AUTHENTICATE {}
DURATION FOR SESSION 2h
;
CREATE user:1;
"#,
&sess,
None,
)
.await
.unwrap();
// Signin with the user
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("id", 1.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"user".to_string(),
vars.into(),
)
.await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected authentication to generally fail, but instead received: {:?}",
res
),
}
}
}
#[tokio::test]
async fn test_signin_bearer_for_user_db() {
// Test with correct bearer key
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON DATABASE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"api".to_string(),
vars.into(),
)
.await;
assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert!(sess.au.is_db());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Expiration should match the defined duration
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to follow the defined duration"
);
}
// Test with correct bearer key and AUTHENTICATE clause succeeding
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON DATABASE TYPE BEARER
AUTHENTICATE {{
RETURN NONE
}}
DURATION FOR SESSION 2h
;
DEFINE USER tobie ON DATABASE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
db: Some("test".to_string()),
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"api".to_string(),
vars.into(),
)
.await;
assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert!(sess.au.is_db());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Expiration should match the defined duration
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to follow the defined duration"
);
}
// Test with correct bearer key and AUTHENTICATE clause failing
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON DATABASE TYPE BEARER
AUTHENTICATE {{
THROW "Test authentication error";
}}
DURATION FOR SESSION 2h
;
DEFINE USER tobie ON DATABASE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
db: Some("test".to_string()),
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"api".to_string(),
vars.into(),
)
.await;
match res {
Err(Error::Thrown(e)) => {
assert_eq!(e, "Test authentication error")
}
res => panic!(
"Expected a thrown authentication error, but instead received: {:?}",
res
),
}
}
// Test with expired grant
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h;
DEFINE USER tobie ON DATABASE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Wait for the grant to expire
std::thread::sleep(Duration::seconds(2).to_std().unwrap());
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"api".to_string(),
vars.into(),
)
.await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
// Test with revoked grant
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h;
DEFINE USER tobie ON DATABASE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Get grant identifier from key
let kid = key.split("-").collect::<Vec<&str>>()[2];
// Revoke grant
ds.execute(
&format!(
r#"
ACCESS api REVOKE `{kid}`;
"#
),
&sess,
None,
)
.await
.unwrap();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"api".to_string(),
vars.into(),
)
.await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
// Test with removed access method
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h;
DEFINE USER tobie ON DATABASE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Remove bearer access method
ds.execute("REMOVE ACCESS api ON DATABASE", &sess, None).await.unwrap();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"api".to_string(),
vars.into(),
)
.await;
match res {
Err(Error::AccessNotFound) => {} // ok
res => panic!(
"Expected an access method not found error, but instead received: {:?}",
res
),
}
}
// Test with missing key
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON DATABASE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let _key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
// The key parameter is not inserted:
let vars: HashMap<&str, Value> = HashMap::new();
// vars.insert("key", key.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"api".to_string(),
vars.into(),
)
.await;
match res {
Err(Error::AccessBearerMissingKey) => {} // ok
res => panic!(
"Expected a missing key authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key prefix part
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON DATABASE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Replace a character from the key prefix
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key[access::GRANT_BEARER_PREFIX.len() - 2] = '_';
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"api".to_string(),
vars.into(),
)
.await;
match res {
Err(Error::AccessGrantBearerInvalid) => {} // ok
res => panic!(
"Expected an invalid key authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key length
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON DATABASE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Remove a character from the bearer key
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key.truncate(access::GRANT_BEARER_LENGTH - 1);
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"api".to_string(),
vars.into(),
)
.await;
match res {
Err(Error::AccessGrantBearerInvalid) => {} // ok
res => panic!(
"Expected an invalid key authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key identifier part
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON DATABASE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Replace a character from the key identifier
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key[access::GRANT_BEARER_PREFIX.len() + 2] = '_';
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"api".to_string(),
vars.into(),
)
.await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key value
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON DATABASE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON DATABASE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Replace a character from the key value
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key
[access::GRANT_BEARER_PREFIX.len() + 1 + access::GRANT_BEARER_ID_LENGTH + 2] = '_';
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = db_access(
&ds,
&mut sess,
"test".to_string(),
"test".to_string(),
"api".to_string(),
vars.into(),
)
.await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
}
#[tokio::test]
async fn test_signin_bearer_for_user_ns() {
// Test with correct bearer key
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON NAMESPACE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res =
ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await;
assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, None);
assert!(sess.au.is_ns());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), None);
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Expiration should match the defined duration
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to follow the defined duration"
);
}
// Test with correct bearer key and AUTHENTICATE clause succeeding
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON NAMESPACE TYPE BEARER
AUTHENTICATE {{
RETURN NONE
}}
DURATION FOR SESSION 2h
;
DEFINE USER tobie ON NAMESPACE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res =
ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await;
assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, None);
assert!(sess.au.is_ns());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), None);
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Expiration should match the defined duration
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to follow the defined duration"
);
}
// Test with correct bearer key and AUTHENTICATE clause failing
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON NAMESPACE TYPE BEARER
AUTHENTICATE {{
THROW "Test authentication error";
}}
DURATION FOR SESSION 2h
;
DEFINE USER tobie ON NAMESPACE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res =
ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await;
match res {
Err(Error::Thrown(e)) => {
assert_eq!(e, "Test authentication error")
}
res => panic!(
"Expected a thrown authentication error, but instead received: {:?}",
res
),
}
}
// Test with expired grant
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h;
DEFINE USER tobie ON NAMESPACE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Wait for the grant to expire
std::thread::sleep(Duration::seconds(2).to_std().unwrap());
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res =
ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
// Test with revoked grant
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h;
DEFINE USER tobie ON NAMESPACE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Get grant identifier from key
let kid = key.split("-").collect::<Vec<&str>>()[2];
// Revoke grant
ds.execute(
&format!(
r#"
ACCESS api REVOKE `{kid}`;
"#
),
&sess,
None,
)
.await
.unwrap();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res =
ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
// Test with removed access method
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h;
DEFINE USER tobie ON NAMESPACE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Remove bearer access method
ds.execute("REMOVE ACCESS api ON NAMESPACE", &sess, None).await.unwrap();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res =
ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await;
match res {
Err(Error::AccessNotFound) => {} // ok
res => panic!(
"Expected an access method not found error, but instead received: {:?}",
res
),
}
}
// Test with missing key
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON NAMESPACE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let _key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
// The key parameter is not inserted:
let vars: HashMap<&str, Value> = HashMap::new();
// vars.insert("key", key.into());
let res =
ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await;
match res {
Err(Error::AccessBearerMissingKey) => {} // ok
res => panic!(
"Expected a missing key authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key prefix part
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON NAMESPACE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Replace a character from the key prefix
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key[access::GRANT_BEARER_PREFIX.len() - 2] = '_';
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res =
ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await;
match res {
Err(Error::AccessGrantBearerInvalid) => {} // ok
res => panic!(
"Expected an invalid key authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key length
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON NAMESPACE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Remove a character from the bearer key
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key.truncate(access::GRANT_BEARER_LENGTH - 1);
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res =
ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await;
match res {
Err(Error::AccessGrantBearerInvalid) => {} // ok
res => panic!(
"Expected an invalid key authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key identifier part
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON NAMESPACE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Replace a character from the key identifier
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key[access::GRANT_BEARER_PREFIX.len() + 2] = '_';
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res =
ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key value
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test");
let res = ds
.execute(
r#"
DEFINE ACCESS api ON NAMESPACE TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON NAMESPACE ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Replace a character from the key value
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key
[access::GRANT_BEARER_PREFIX.len() + 1 + access::GRANT_BEARER_ID_LENGTH + 2] = '_';
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res =
ns_access(&ds, &mut sess, "test".to_string(), "api".to_string(), vars.into()).await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
}
#[tokio::test]
async fn test_signin_bearer_for_user_root() {
// Test with correct bearer key
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
let res = ds
.execute(
r#"
DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON ROOT ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await;
assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res);
assert_eq!(sess.ns, None);
assert_eq!(sess.db, None);
assert!(sess.au.is_root());
assert_eq!(sess.au.level().ns(), None);
assert_eq!(sess.au.level().db(), None);
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Expiration should match the defined duration
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to follow the defined duration"
);
}
// Test with correct bearer key and AUTHENTICATE clause succeeding
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
let res = ds
.execute(
r#"
DEFINE ACCESS api ON ROOT TYPE BEARER
AUTHENTICATE {{
RETURN NONE
}}
DURATION FOR SESSION 2h
;
DEFINE USER tobie ON ROOT ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await;
assert!(res.is_ok(), "Failed to signin with bearer key: {:?}", res);
assert_eq!(sess.ns, None);
assert_eq!(sess.db, None);
assert!(sess.au.is_root());
assert_eq!(sess.au.level().ns(), None);
assert_eq!(sess.au.level().db(), None);
// Record users should not have roles
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not have Viewer role");
assert!(sess.au.has_role(&Role::Editor), "Auth user expected to have Editor role");
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
// Expiration should match the defined duration
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to follow the defined duration"
);
}
// Test with correct bearer key and AUTHENTICATE clause failing
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
let res = ds
.execute(
r#"
DEFINE ACCESS api ON ROOT TYPE BEARER
AUTHENTICATE {{
THROW "Test authentication error";
}}
DURATION FOR SESSION 2h
;
DEFINE USER tobie ON ROOT ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await;
match res {
Err(Error::Thrown(e)) => {
assert_eq!(e, "Test authentication error")
}
res => panic!(
"Expected a thrown authentication error, but instead received: {:?}",
res
),
}
}
// Test with expired grant
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
let res = ds
.execute(
r#"
DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h;
DEFINE USER tobie ON ROOT ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Wait for the grant to expire
std::thread::sleep(Duration::seconds(2).to_std().unwrap());
// Signin with the bearer key
let mut sess = Session {
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
// Test with revoked grant
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
let res = ds
.execute(
r#"
DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h;
DEFINE USER tobie ON ROOT ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Get grant identifier from key
let kid = key.split("-").collect::<Vec<&str>>()[2];
// Revoke grant
ds.execute(
&format!(
r#"
ACCESS api REVOKE `{kid}`;
"#
),
&sess,
None,
)
.await
.unwrap();
// Signin with the bearer key
let mut sess = Session {
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
// Test with removed access method
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
let res = ds
.execute(
r#"
DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR GRANT 1s FOR SESSION 2h;
DEFINE USER tobie ON ROOT ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let key = grant.get("key").unwrap().clone().as_string();
// Remove bearer access method
ds.execute("REMOVE ACCESS api ON ROOT", &sess, None).await.unwrap();
// Signin with the bearer key
let mut sess = Session {
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await;
match res {
Err(Error::AccessNotFound) => {} // ok
res => panic!(
"Expected an access method not found error, but instead received: {:?}",
res
),
}
}
// Test with missing key
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
let res = ds
.execute(
r#"
DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON ROOT ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let _key = grant.get("key").unwrap().clone().as_string();
// Signin with the bearer key
let mut sess = Session {
..Default::default()
};
// The key parameter is not inserted:
let vars: HashMap<&str, Value> = HashMap::new();
// vars.insert("key", key.into());
let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await;
match res {
Err(Error::AccessBearerMissingKey) => {} // ok
res => panic!(
"Expected a missing key authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key prefix part
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
let res = ds
.execute(
r#"
DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON ROOT ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Replace a character from the key prefix
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key[access::GRANT_BEARER_PREFIX.len() - 2] = '_';
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await;
match res {
Err(Error::AccessGrantBearerInvalid) => {} // ok
res => panic!(
"Expected an invalid key authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key length
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
let res = ds
.execute(
r#"
DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON ROOT ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Remove a character from the bearer key
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key.truncate(access::GRANT_BEARER_LENGTH - 1);
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await;
match res {
Err(Error::AccessGrantBearerInvalid) => {} // ok
res => panic!(
"Expected an invalid key authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key identifier part
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
let res = ds
.execute(
r#"
DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON ROOT ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Replace a character from the key identifier
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key[access::GRANT_BEARER_PREFIX.len() + 2] = '_';
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
// Test with incorrect bearer key value
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner();
let res = ds
.execute(
r#"
DEFINE ACCESS api ON ROOT TYPE BEARER DURATION FOR SESSION 2h;
DEFINE USER tobie ON ROOT ROLES EDITOR;
ACCESS api GRANT FOR USER tobie;
"#,
&sess,
None,
)
.await
.unwrap();
// Get the bearer key from grant.
let result = if let Ok(res) = &res.last().unwrap().result {
res.clone()
} else {
panic!("Unable to retrieve bearer key grant");
};
let grant = result
.coerce_to_object()
.unwrap()
.get("grant")
.unwrap()
.clone()
.coerce_to_object()
.unwrap();
let valid_key = grant.get("key").unwrap().clone().as_string();
// Replace a character from the key value
let mut invalid_key: Vec<char> = valid_key.chars().collect();
invalid_key
[access::GRANT_BEARER_PREFIX.len() + 1 + access::GRANT_BEARER_ID_LENGTH + 2] = '_';
let key: String = invalid_key.into_iter().collect();
// Signin with the bearer key
let mut sess = Session {
..Default::default()
};
let mut vars: HashMap<&str, Value> = HashMap::new();
vars.insert("key", key.into());
let res = root_access(&ds, &mut sess, "api".to_string(), vars.into()).await;
match res {
Err(Error::InvalidAuth) => {} // ok
res => panic!(
"Expected a generic authentication error, but instead received: {:?}",
res
),
}
}
}
}