use std::sync::Arc;
use anyhow::{Result, bail};
use reblessive;
use surrealdb_types::ToSql;
use crate::catalog;
use crate::catalog::base::Base as AccessBase;
use crate::catalog::providers::{AuthorisationProvider, DatabaseProvider};
use crate::dbs::{Session, SessionData};
use crate::err::exec_error;
use crate::exec::Error as ExecError;
use crate::expr::statements::access;
use crate::expr::{Base, Expr};
use crate::iam::token::Claims;
use crate::iam::{Error as AuthError, Level, Role};
use crate::kvs::TransactionType::*;
use crate::kvs::{Datastore, is_retryable_transaction_conflict};
use crate::types::{PublicRecordId, PublicValue};
use crate::val::RecordId;
pub(crate) fn reset_previous_principal(session: &mut Session) {
session.ac = None;
session.rd = None;
session.data = None;
}
pub(crate) fn access_session(
de: &catalog::AccessDefinition,
ns: Option<&str>,
db: Option<&str>,
caller: &Session,
) -> Result<Session> {
let level = match (&de.base, ns, db) {
(AccessBase::Root, None, None) => Level::Root,
(AccessBase::Ns, Some(ns), None) => Level::Namespace(ns.to_owned()),
(AccessBase::Db, Some(ns), Some(db)) => Level::Database(ns.to_owned(), db.to_owned()),
(base, ns, db) => {
warn!(
"Access method `{}` is defined at {base} level but was resolved with namespace {ns:?} and database {db:?}",
de.name
);
bail!(AuthError::InvalidAuth)
}
};
let mut sess = Session::for_level(level, Role::Editor);
sess.ip.clone_from(&caller.ip);
sess.or.clone_from(&caller.or);
Ok(sess)
}
pub(crate) fn context_session(
de: &catalog::AccessDefinition,
ns: &str,
db: &str,
rid: PublicValue,
token: PublicValue,
caller: &Session,
) -> Session {
let mut sess = Session::for_record(ns, db, de.name.as_str(), rid);
sess.tk = Some(token);
sess.ip.clone_from(&caller.ip);
sess.or.clone_from(&caller.or);
sess
}
pub(crate) async fn compute_access_context(
kvs: &Datastore,
session: &Session,
context: &Expr,
) -> Result<PublicValue> {
match kvs.evaluate(context, session, None).await {
Ok(val) => Ok(val),
Err(e) if matches!(exec_error(&e), Some(ExecError::Thrown(_))) => Err(e),
Err(e) => {
if is_retryable_transaction_conflict(&e) {
debug!("Unexpected error found while executing a CONTEXT clause: {e}");
Err(anyhow::Error::new(AuthError::UnexpectedAuth))
} else {
debug!("Authentication attempt failed due to an error in the CONTEXT clause: {e}");
if kvs.config().iam.insecure_forward_access_errors {
Err(e)
} else {
Err(anyhow::Error::new(AuthError::InvalidAuth))
}
}
}
}
}
pub(crate) async fn session_data_for_record(
kvs: &Datastore,
de: &catalog::AccessDefinition,
ns: &str,
db: &str,
rid: PublicValue,
claims: &Claims,
caller: &Session,
) -> Result<Option<SessionData>> {
let Some(context) = &de.context else {
return Ok(None);
};
if !context.read_only() {
warn!(
"The stored CONTEXT clause of access method `{}` is not read-only and will not be evaluated",
de.name
);
bail!(AuthError::InvalidAuth);
}
let mut claims = claims.clone();
claims.id = Some(rid.to_sql());
let token = crate::val::convert_value_to_public_value(claims.into_claims_object().into())?;
let sess = context_session(de, ns, db, rid, token, caller);
let val = compute_access_context(kvs, &sess, context).await?;
Ok(Some(SessionData::new(val)))
}
pub(crate) async fn authenticate_record(
kvs: &Datastore,
session: &Session,
authenticate: &Expr,
) -> Result<PublicRecordId> {
match kvs.evaluate(authenticate, session, None).await {
Ok(val) => match val.into_record() {
Ok(id) => Ok(id),
_ => {
debug!("Authentication attempt as record user rejected by AUTHENTICATE clause");
Err(anyhow::Error::new(AuthError::InvalidAuth))
}
},
Err(e) if matches!(exec_error(&e), Some(ExecError::Thrown(_))) => Err(e),
Err(e) => {
if is_retryable_transaction_conflict(&e) {
debug!("Unexpected error found while executing AUTHENTICATE clause: {e}");
Err(anyhow::Error::new(AuthError::UnexpectedAuth))
} else {
debug!(
"Authentication attempt failed due to an error in the AUTHENTICATE clause: {e}"
);
if kvs.config().iam.insecure_forward_access_errors {
Err(e)
} else {
Err(anyhow::Error::new(AuthError::InvalidAuth))
}
}
}
}
}
pub(crate) async fn authenticate_generic(
kvs: &Datastore,
session: &Session,
authenticate: &Expr,
) -> Result<()> {
match kvs.evaluate(authenticate, session, None).await {
Ok(val) => {
match val {
PublicValue::None => Ok(()),
_ => {
debug!("Authentication attempt as system user rejected by AUTHENTICATE clause");
Err(anyhow::Error::new(AuthError::InvalidAuth))
}
}
}
Err(e) if matches!(exec_error(&e), Some(ExecError::Thrown(_))) => Err(e),
Err(e) => {
if is_retryable_transaction_conflict(&e) {
debug!("Unexpected error found while executing an AUTHENTICATE clause: {e}");
Err(anyhow::Error::new(AuthError::UnexpectedAuth))
} else {
debug!(
"Authentication attempt failed due to an error in the AUTHENTICATE clause: {e}"
);
if kvs.config().iam.insecure_forward_access_errors {
Err(e)
} else {
Err(anyhow::Error::new(AuthError::InvalidAuth))
}
}
}
}
}
pub(crate) async fn create_refresh_token_record(
kvs: &Datastore,
ac: String,
ns: &str,
db: &str,
rid: RecordId,
) -> Result<String> {
let sess = Session::owner().with_ns(ns).with_db(db);
let opt = kvs.setup_options(&sess);
let mut ctx = kvs.setup_ctx()?;
let tx = kvs.transaction(Write).await?.enclose();
ctx.set_transaction(Arc::clone(&tx));
let ctx = ctx.freeze();
let grant = run!(
tx,
crate::legacy::create_grant(ac, Some(Base::Db), catalog::Subject::Record(rid), &ctx, &opt)
.await
.map_err(|e| {
warn!("Unexpected error when attempting to create a refresh token: {e}");
anyhow::Error::new(AuthError::UnexpectedAuth)
})
)?;
match grant.grant {
catalog::Grant::Bearer(bearer) => Ok(bearer.key),
_ => Err(anyhow::Error::new(AuthError::AccessMethodMismatch)),
}
}
pub async fn verify_and_revoke_refresh_token_record(
kvs: &Datastore,
gr: String,
ac: String,
refresh: String,
ns: &str,
db: &str,
) -> Result<()> {
let tx = kvs.transaction(Read).await?;
let db_def = match tx.get_db_by_name(ns, db, None).await {
Ok(Some(db_def)) => db_def,
Ok(None) => {
let _ = tx.cancel().await;
debug!("Refresh token revocation rejected: database `{ns}/{db}` not found");
return Err(anyhow::Error::new(AuthError::InvalidAuth));
}
Err(e) => {
let _ = tx.cancel().await;
return Err(e);
}
};
let grant = match tx
.get_db_access_grant(db_def.namespace_id, db_def.database_id, &ac, &gr, None)
.await
{
Ok(Some(grant)) => grant,
Ok(None) => {
let _ = tx.cancel().await;
debug!("Refresh token revocation rejected: grant `{gr}` for method `{ac}` not found");
return Err(anyhow::Error::new(AuthError::InvalidAuth));
}
Err(e) => {
let _ = tx.cancel().await;
return Err(e);
}
};
tx.cancel().await?;
crate::iam::signin::verify_grant_bearer(&grant, refresh)?;
revoke_refresh_token_record(kvs, gr, ac, ns, db).await
}
pub async fn revoke_refresh_token_record(
kvs: &Datastore,
gr: String,
ac: String,
ns: &str,
db: &str,
) -> Result<()> {
let stmt = access::AccessStatementRevoke {
ac: ac.into(),
base: Some(Base::Db),
gr: Some(gr.into()),
cond: None,
};
let sess = Session::owner().with_ns(ns).with_db(db);
let opt = kvs.setup_options(&sess);
let mut ctx = kvs.setup_ctx()?;
let tx = kvs.transaction(Write).await?.enclose();
ctx.set_transaction(Arc::clone(&tx));
let ctx = ctx.freeze();
let mut stack = reblessive::tree::TreeStack::new();
run!(
tx,
stack
.enter(|stk| async {
crate::legacy::revoke_grant(&stmt, stk, &ctx, &opt).await.map_err(|e| {
warn!("Unexpected error when attempting to revoke a refresh token: {e}");
anyhow::Error::new(AuthError::UnexpectedAuth)
})
})
.finish()
.await
)?;
Ok(())
}
#[cfg(test)]
mod read_only_backstop_tests {
use catalog::ExprText;
use super::*;
use crate::iam::token::Claims;
fn definition(context: &str) -> catalog::AccessDefinition {
catalog::AccessDefinition {
name: "api".into(),
base: AccessBase::Db,
access_type: catalog::AccessType::Record(catalog::RecordAccess {
signup: None,
signin: None,
jwt: catalog::JwtAccess {
verify: catalog::JwtAccessVerify::Key(catalog::JwtAccessVerifyKey {
alg: catalog::Algorithm::Hs512,
key: "secret".to_owned(),
}),
issue: None,
audience: None,
},
bearer: None,
}),
authenticate: None,
context: Some(ExprText::from_raw(context).compile().unwrap()),
signup: None,
signin: None,
grant_duration: None,
token_duration: None,
session_duration: None,
comment: None,
}
}
#[tokio::test]
async fn a_stored_clause_that_writes_is_refused_at_evaluation() {
let ds = Datastore::new("memory").await.unwrap();
let owner = Session::owner().with_ns("test").with_db("test");
for res in ds
.execute(
"DEFINE NAMESPACE test; DEFINE DATABASE test; DEFINE TABLE audit SCHEMALESS PERMISSIONS FULL",
&owner,
None,
)
.await
.unwrap()
{
res.result.unwrap();
}
let de = definition("CREATE audit SET at = 1");
let rid = PublicValue::RecordId(surrealdb_types::RecordId::new("user", 1));
let err = session_data_for_record(
&ds,
&de,
"test",
"test",
rid,
&Claims::default(),
&Session::default(),
)
.await
.unwrap_err();
assert!(
err.downcast_ref::<AuthError>().is_some(),
"a non-read-only stored clause must fail authentication, got: {err}"
);
let res = ds.execute("SELECT VALUE id FROM audit", &owner, None).await.unwrap();
let rows = res.into_iter().next().unwrap().result.unwrap().into_array().unwrap().into_vec();
assert!(rows.is_empty(), "the refused clause must not have written anything");
}
#[tokio::test]
async fn a_stored_read_only_clause_still_evaluates() {
let ds = Datastore::new("memory").await.unwrap();
let de = definition("RETURN 'granted'");
let rid = PublicValue::RecordId(surrealdb_types::RecordId::new("user", 1));
let data = session_data_for_record(
&ds,
&de,
"test",
"test",
rid,
&Claims::default(),
&Session::default(),
)
.await
.unwrap()
.expect("a read-only clause produces a payload");
assert_eq!(data.public(), &PublicValue::String("granted".to_owned()));
}
}