use anyhow::{Result, bail};
use argon2::Argon2;
use argon2::password_hash::{PasswordHasher, SaltString};
use rand::distr::{Alphanumeric, SampleString};
use rand_core::OsRng;
use reblessive::tree::Stk;
use surrealdb_types::{SqlFormat, ToSql};
use super::DefineKind;
use crate::catalog::providers::{CatalogProvider, NamespaceProvider, UserProvider};
use crate::catalog::{self, UserDefinition};
use crate::ctx::FrozenContext;
use crate::dbs::Options;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::expr::parameterize::expr_to_ident;
use crate::expr::user::UserDuration;
use crate::expr::{Base, Expr, FlowResultExt, Idiom, Literal};
use crate::iam::{Action, ResourceKind};
use crate::val::{self, Duration, Value};
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct DefineUserStatement {
pub kind: DefineKind,
pub name: Expr,
pub base: Base,
pub hash: String,
pub code: String,
pub roles: Vec<String>,
pub duration: UserDuration,
pub comment: Expr,
}
impl Default for DefineUserStatement {
fn default() -> Self {
Self {
kind: DefineKind::Default,
name: Expr::Literal(Literal::None),
base: Base::Root,
hash: String::new(),
code: String::new(),
roles: vec![],
duration: UserDuration::default(),
comment: Expr::Literal(Literal::None),
}
}
}
impl DefineUserStatement {
pub(crate) fn new_with_password(base: Base, user: String, pass: &str, role: String) -> Self {
DefineUserStatement {
kind: DefineKind::Default,
base,
name: Expr::Idiom(Idiom::field(user)),
hash: Argon2::default()
.hash_password(pass.as_ref(), &SaltString::generate(&mut OsRng))
.expect("password hashing should not fail")
.to_string(),
code: Alphanumeric.sample_string(&mut rand::rng(), 128),
roles: vec![role],
duration: UserDuration::default(),
comment: Expr::Literal(Literal::None),
}
}
pub(crate) async fn to_definition(
&self,
stk: &mut Stk,
ctx: &FrozenContext,
opt: &Options,
doc: Option<&CursorDoc>,
) -> Result<catalog::UserDefinition> {
let token_duration = stk
.run(|stk| self.duration.token.compute(stk, ctx, opt, doc))
.await
.catch_return()?
.cast_to::<Option<Duration>>()?
.map(|x| x.0);
let session_duration = stk
.run(|stk| self.duration.session.compute(stk, ctx, opt, doc))
.await
.catch_return()?
.cast_to::<Option<Duration>>()?
.map(|x| x.0);
let comment = stk
.run(|stk| self.comment.compute(stk, ctx, opt, doc))
.await
.catch_return()?
.cast_to()?;
Ok(UserDefinition {
name: expr_to_ident(stk, ctx, opt, doc, &self.name, "user name").await?.into(),
hash: self.hash.clone(),
code: self.code.clone(),
roles: self.roles.clone(),
token_duration,
session_duration,
comment,
base: self.base.into(),
})
}
pub fn from_definition(base: Base, def: &catalog::UserDefinition) -> Self {
Self {
kind: DefineKind::Default,
base,
name: Expr::Idiom(Idiom::field(def.name.clone())),
hash: def.hash.clone(),
code: def.code.clone(),
roles: def.roles.clone(),
duration: UserDuration {
token: def
.token_duration
.map(|x| Expr::Literal(Literal::Duration(val::Duration(x))))
.unwrap_or(Expr::Literal(Literal::None)),
session: def
.session_duration
.map(|x| Expr::Literal(Literal::Duration(val::Duration(x))))
.unwrap_or(Expr::Literal(Literal::None)),
},
comment: def
.comment
.as_ref()
.map(|x| Expr::Idiom(Idiom::field(x.clone())))
.unwrap_or(Expr::Literal(Literal::None)),
}
}
#[instrument(level = "trace", name = "DefineUserStatement::compute", skip_all)]
pub(crate) async fn compute(
&self,
stk: &mut Stk,
ctx: &FrozenContext,
opt: &Options,
doc: Option<&CursorDoc>,
) -> Result<Value> {
ctx.is_allowed(opt, Action::Edit, ResourceKind::Actor, self.base)?;
let definition = self.to_definition(stk, ctx, opt, doc).await?;
match self.base {
Base::Root => {
let txn = ctx.tx();
if let Some(user) = txn.get_root_user(definition.name.as_str(), None).await? {
match self.kind {
DefineKind::Default => {
if !opt.import {
bail!(Error::UserRootAlreadyExists {
name: user.name.to_string(),
});
}
}
DefineKind::Overwrite => {}
DefineKind::IfNotExists => return Ok(Value::None),
}
}
txn.put_root_user(&definition).await?;
txn.clear_cache();
Ok(Value::None)
}
Base::Ns => {
let txn = ctx.tx();
let ns = ctx.get_ns_id(opt).await?;
if let Some(user) = txn.get_ns_user(ns, definition.name.as_str(), None).await? {
match self.kind {
DefineKind::Default => {
if !opt.import {
bail!(Error::UserNsAlreadyExists {
name: user.name.to_string(),
ns: opt.ns()?.into(),
});
}
}
DefineKind::Overwrite => {}
DefineKind::IfNotExists => return Ok(Value::None),
}
}
let ns = {
let ns = opt.ns()?;
txn.get_or_add_ns(Some(ctx), ns).await?
};
txn.put_ns_user(ns.namespace_id, &definition).await?;
txn.clear_cache();
Ok(Value::None)
}
Base::Db => {
let txn = ctx.tx();
let (ns, db) = ctx.get_ns_db_ids(opt).await?;
if let Some(user) = txn.get_db_user(ns, db, definition.name.as_str(), None).await? {
match self.kind {
DefineKind::Default => {
if !opt.import {
bail!(Error::UserDbAlreadyExists {
name: user.name.to_string(),
ns: opt.ns()?.to_string(),
db: opt.db()?.to_string(),
});
}
}
DefineKind::Overwrite => {}
DefineKind::IfNotExists => return Ok(Value::None),
}
}
let db = {
let (ns, db) = opt.ns_db()?;
txn.get_or_add_db(Some(ctx), ns, db).await?
};
txn.put_db_user(db.namespace_id, db.database_id, &definition).await?;
txn.clear_cache();
Ok(Value::None)
}
}
}
}
impl ToSql for DefineUserStatement {
fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
let stmt: crate::sql::statements::define::DefineUserStatement = self.clone().into();
stmt.fmt_sql(f, fmt);
}
}