use anyhow::{Result, bail};
use rand::distr::{Alphanumeric, SampleString};
use reblessive::tree::Stk;
use surrealdb_strand::Strand;
use surrealdb_types::{SqlFormat, ToSql};
use super::DefineKind;
use crate::catalog::providers::{AuthorisationProvider, NamespaceProvider};
use crate::catalog::{self, AccessDefinition};
use crate::ctx::FrozenContext;
use crate::dbs::Options;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::expr::access::AccessDuration;
use crate::expr::access_type::{
BearerAccess, BearerAccessSubject, BearerAccessType, JwtAccessIssue, JwtAccessVerify,
JwtAccessVerifyJwks, JwtAccessVerifyKey,
};
use crate::expr::parameterize::expr_to_ident;
use crate::expr::{
AccessType, Algorithm, Base, Expr, FlowResultExt, Idiom, JwtAccess, Literal, RecordAccess,
};
use crate::iam::{Action, ResourceKind};
use crate::val::{self, Duration, Value};
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct DefineAccessStatement {
pub kind: DefineKind,
pub name: Expr,
pub base: Base,
pub access_type: AccessType,
pub authenticate: Option<Expr>,
pub duration: AccessDuration,
pub comment: Expr,
}
impl Default for DefineAccessStatement {
fn default() -> Self {
Self {
kind: DefineKind::Default,
name: Expr::Literal(Literal::None),
base: Base::Root,
access_type: AccessType::default(),
authenticate: None,
duration: AccessDuration::default(),
comment: Expr::Literal(Literal::None),
}
}
}
impl DefineAccessStatement {
pub(crate) fn random_key() -> String {
Alphanumeric.sample_string(&mut rand::rng(), 128)
}
pub fn from_definition(base: Base, def: &AccessDefinition) -> Self {
fn convert_algorithm(access: catalog::Algorithm) -> Algorithm {
match &access {
catalog::Algorithm::EdDSA => Algorithm::EdDSA,
catalog::Algorithm::Es256 => Algorithm::Es256,
catalog::Algorithm::Es384 => Algorithm::Es384,
catalog::Algorithm::Es512 => Algorithm::Es512,
catalog::Algorithm::Hs256 => Algorithm::Hs256,
catalog::Algorithm::Hs384 => Algorithm::Hs384,
catalog::Algorithm::Hs512 => Algorithm::Hs512,
catalog::Algorithm::Ps256 => Algorithm::Ps256,
catalog::Algorithm::Ps384 => Algorithm::Ps384,
catalog::Algorithm::Ps512 => Algorithm::Ps512,
catalog::Algorithm::Rs256 => Algorithm::Rs256,
catalog::Algorithm::Rs384 => Algorithm::Rs384,
catalog::Algorithm::Rs512 => Algorithm::Rs512,
}
}
fn convert_jwt_access(access: &catalog::JwtAccess) -> JwtAccess {
JwtAccess {
verify: match &access.verify {
catalog::JwtAccessVerify::Key(k) => JwtAccessVerify::Key(JwtAccessVerifyKey {
alg: convert_algorithm(k.alg),
key: Expr::Literal(Literal::String(k.key.as_str().into())),
}),
catalog::JwtAccessVerify::Jwks(j) => {
JwtAccessVerify::Jwks(JwtAccessVerifyJwks {
url: Expr::Literal(Literal::String(j.url.as_str().into())),
})
}
},
issue: access.issue.as_ref().map(|x| JwtAccessIssue {
alg: convert_algorithm(x.alg),
key: Expr::Literal(Literal::String(x.key.as_str().into())),
}),
}
}
fn convert_bearer_access(access: &catalog::BearerAccess) -> BearerAccess {
BearerAccess {
kind: match access.kind {
catalog::BearerAccessType::Bearer => BearerAccessType::Bearer,
catalog::BearerAccessType::Refresh => BearerAccessType::Refresh,
},
subject: match access.subject {
catalog::BearerAccessSubject::Record => BearerAccessSubject::Record,
catalog::BearerAccessSubject::User => BearerAccessSubject::User,
},
jwt: convert_jwt_access(&access.jwt),
}
}
DefineAccessStatement {
kind: DefineKind::Default,
base,
name: Expr::Idiom(Idiom::field(def.name.clone())),
duration: AccessDuration {
grant: def
.grant_duration
.map(|v| Expr::Literal(Literal::Duration(val::Duration(v))))
.unwrap_or(Expr::Literal(Literal::None)),
token: def
.token_duration
.map(|v| Expr::Literal(Literal::Duration(val::Duration(v))))
.unwrap_or(Expr::Literal(Literal::None)),
session: def
.session_duration
.map(|v| Expr::Literal(Literal::Duration(val::Duration(v))))
.unwrap_or(Expr::Literal(Literal::None)),
},
comment: def
.comment
.clone()
.map(|x| Expr::Literal(Literal::String(x.into())))
.unwrap_or(Expr::Literal(Literal::None)),
authenticate: def.authenticate.clone(),
access_type: match &def.access_type {
catalog::AccessType::Record(record_access) => {
AccessType::Record(Box::new(RecordAccess {
signup: record_access.signup.clone(),
signin: record_access.signin.clone(),
jwt: convert_jwt_access(&record_access.jwt),
bearer: record_access.bearer.as_ref().map(convert_bearer_access),
}))
}
catalog::AccessType::Jwt(jwt_access) => {
AccessType::Jwt(convert_jwt_access(jwt_access))
}
catalog::AccessType::Bearer(bearer_access) => {
AccessType::Bearer(convert_bearer_access(bearer_access))
}
},
}
}
async fn to_definition(
&self,
stk: &mut Stk,
ctx: &FrozenContext,
opt: &Options,
doc: Option<&CursorDoc>,
) -> Result<AccessDefinition> {
fn convert_algorithm(access: Algorithm) -> catalog::Algorithm {
match &access {
Algorithm::EdDSA => catalog::Algorithm::EdDSA,
Algorithm::Es256 => catalog::Algorithm::Es256,
Algorithm::Es384 => catalog::Algorithm::Es384,
Algorithm::Es512 => catalog::Algorithm::Es512,
Algorithm::Hs256 => catalog::Algorithm::Hs256,
Algorithm::Hs384 => catalog::Algorithm::Hs384,
Algorithm::Hs512 => catalog::Algorithm::Hs512,
Algorithm::Ps256 => catalog::Algorithm::Ps256,
Algorithm::Ps384 => catalog::Algorithm::Ps384,
Algorithm::Ps512 => catalog::Algorithm::Ps512,
Algorithm::Rs256 => catalog::Algorithm::Rs256,
Algorithm::Rs384 => catalog::Algorithm::Rs384,
Algorithm::Rs512 => catalog::Algorithm::Rs512,
}
}
async fn convert_jwt_access(
stk: &mut Stk,
ctx: &FrozenContext,
opt: &Options,
doc: Option<&CursorDoc>,
access: &JwtAccess,
) -> Result<catalog::JwtAccess> {
let verify = match &access.verify {
JwtAccessVerify::Key(k) => {
catalog::JwtAccessVerify::Key(catalog::JwtAccessVerifyKey {
alg: convert_algorithm(k.alg),
key: stk
.run(|stk| k.key.compute(stk, ctx, opt, doc))
.await
.catch_return()?
.coerce_to::<String>()?,
})
}
JwtAccessVerify::Jwks(j) => {
catalog::JwtAccessVerify::Jwks(catalog::JwtAccessVerifyJwks {
url: stk
.run(|stk| j.url.compute(stk, ctx, opt, doc))
.await
.catch_return()?
.cast_to()?,
})
}
};
let issue = map_opt!(x as &access.issue => catalog::JwtAccessIssue {
alg: convert_algorithm(x.alg),
key: stk.run(|stk| x.key.compute(stk, ctx, opt, doc)).await.catch_return()?.cast_to()?,
});
if let (catalog::JwtAccessVerify::Key(ver), Some(iss)) = (&verify, &issue)
&& ver.alg.is_symmetric()
&& ver.key != iss.key
{
bail!(Error::Query {
message: format!(
"Symmetric algorithm {} requires the same key for signing and verification. \
Use the same key value for both KEY and WITH ISSUER KEY clauses, or omit WITH ISSUER KEY.",
ver.alg
)
});
}
Ok(catalog::JwtAccess {
verify,
issue,
})
}
async fn convert_bearer_access(
stk: &mut Stk,
ctx: &FrozenContext,
opt: &Options,
doc: Option<&CursorDoc>,
access: &BearerAccess,
) -> Result<catalog::BearerAccess> {
Ok(catalog::BearerAccess {
kind: match access.kind {
BearerAccessType::Bearer => catalog::BearerAccessType::Bearer,
BearerAccessType::Refresh => catalog::BearerAccessType::Refresh,
},
subject: match access.subject {
BearerAccessSubject::Record => catalog::BearerAccessSubject::Record,
BearerAccessSubject::User => catalog::BearerAccessSubject::User,
},
jwt: convert_jwt_access(stk, ctx, opt, doc, &access.jwt).await?,
})
}
let grant_duration = stk
.run(|stk| self.duration.grant.compute(stk, ctx, opt, doc))
.await
.catch_return()?
.cast_to::<Option<Duration>>()?
.map(|x| x.0);
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);
if matches!(&self.access_type, AccessType::Record(_)) && token_duration.is_none() {
bail!(Error::AccessRecordTokenDurationRequired);
}
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(AccessDefinition {
name: expr_to_ident(stk, ctx, opt, doc, &self.name, "access name").await?.into(),
base: self.base.into(),
grant_duration,
token_duration,
session_duration,
comment,
authenticate: self.authenticate.clone(),
access_type: match &self.access_type {
AccessType::Record(record_access) => {
catalog::AccessType::Record(catalog::RecordAccess {
signup: record_access.signup.clone(),
signin: record_access.signin.clone(),
jwt: convert_jwt_access(stk, ctx, opt, doc, &record_access.jwt).await?,
bearer: map_opt!(x as &record_access.bearer => convert_bearer_access(stk, ctx, opt, doc, x).await?),
})
}
AccessType::Jwt(jwt_access) => catalog::AccessType::Jwt(
convert_jwt_access(stk, ctx, opt, doc, jwt_access).await?,
),
AccessType::Bearer(bearer_access) => catalog::AccessType::Bearer(
convert_bearer_access(stk, ctx, opt, doc, bearer_access).await?,
),
},
})
}
}
impl DefineAccessStatement {
fn uses_es512(definition: &AccessDefinition) -> bool {
fn jwt_uses_es512(jwt: &catalog::JwtAccess) -> bool {
if let catalog::JwtAccessVerify::Key(ref ver) = jwt.verify
&& matches!(ver.alg, catalog::Algorithm::Es512)
{
return true;
}
if let Some(ref iss) = jwt.issue
&& matches!(iss.alg, catalog::Algorithm::Es512)
{
return true;
}
false
}
match &definition.access_type {
catalog::AccessType::Jwt(jwt) => jwt_uses_es512(jwt),
catalog::AccessType::Record(rec) => {
jwt_uses_es512(&rec.jwt)
|| rec.bearer.as_ref().is_some_and(|b| jwt_uses_es512(&b.jwt))
}
catalog::AccessType::Bearer(bearer) => jwt_uses_es512(&bearer.jwt),
}
}
fn reject_es512(definition: &AccessDefinition) -> Result<()> {
if Self::uses_es512(definition) {
bail!(Error::AccessUnsupportedAlgorithm);
}
Ok(())
}
#[instrument(level = "trace", name = "DefineAccessStatement::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();
let mut existing_uses_es512 = false;
if let Some(access) = txn.get_root_access(definition.name.as_str(), None).await? {
existing_uses_es512 = Self::uses_es512(&access);
match self.kind {
DefineKind::Default => {
if !opt.import {
bail!(Error::AccessRootAlreadyExists {
ac: access.name.to_string(),
});
}
}
DefineKind::Overwrite => {}
DefineKind::IfNotExists => return Ok(Value::None),
}
}
if !(opt.import || (existing_uses_es512 && self.kind == DefineKind::Overwrite)) {
Self::reject_es512(&definition)?;
}
let key = crate::key::root::ac::new(definition.name.as_str());
txn.set(&key, &definition).await?;
txn.clear_cache();
Ok(Value::None)
}
Base::Ns => {
let txn = ctx.tx();
let ns = ctx.get_ns_id(opt).await?;
let mut existing_uses_es512 = false;
if let Some(access) = txn.get_ns_access(ns, definition.name.as_str(), None).await? {
existing_uses_es512 = Self::uses_es512(&access);
match self.kind {
DefineKind::Default => {
if !opt.import {
bail!(Error::AccessNsAlreadyExists {
ns: opt.ns()?.to_string(),
ac: access.name.to_string(),
});
}
}
DefineKind::Overwrite => {}
DefineKind::IfNotExists => return Ok(Value::None),
}
}
if !(opt.import || (existing_uses_es512 && self.kind == DefineKind::Overwrite)) {
Self::reject_es512(&definition)?;
}
let key = crate::key::namespace::ac::new(ns, definition.name.as_str());
txn.get_or_add_ns(Some(ctx), opt.ns()?).await?;
txn.set(&key, &definition).await?;
txn.clear_cache();
Ok(Value::None)
}
Base::Db => {
let txn = ctx.tx();
let (ns, db) = ctx.get_ns_db_ids(opt).await?;
let mut existing_uses_es512 = false;
if let Some(access) =
txn.get_db_access(ns, db, definition.name.as_str(), None).await?
{
existing_uses_es512 = Self::uses_es512(&access);
match self.kind {
DefineKind::Default => {
if !opt.import {
bail!(Error::AccessDbAlreadyExists {
ns: opt.ns()?.to_string(),
db: opt.db()?.to_string(),
ac: access.name.to_string(),
});
}
}
DefineKind::Overwrite => {}
DefineKind::IfNotExists => return Ok(Value::None),
}
}
if !(opt.import || (existing_uses_es512 && self.kind == DefineKind::Overwrite)) {
Self::reject_es512(&definition)?;
}
let key = crate::key::database::ac::new(ns, db, definition.name.as_str());
txn.set(&key, &definition).await?;
txn.clear_cache();
Ok(Value::None)
}
}
}
pub fn redact(mut self) -> Self {
fn redact_jwt_access(acc: &mut JwtAccess) {
if let JwtAccessVerify::Key(ref mut v) = acc.verify
&& v.alg.is_symmetric()
{
v.key = Expr::Literal(Literal::String(Strand::new_static("[REDACTED]")));
}
if let Some(ref mut s) = acc.issue {
s.key = Expr::Literal(Literal::String(Strand::new_static("[REDACTED]")));
}
}
match self.access_type {
AccessType::Jwt(ref mut key) => {
redact_jwt_access(key);
}
AccessType::Bearer(ref mut b) => {
redact_jwt_access(&mut b.jwt);
}
AccessType::Record(ref mut r) => {
redact_jwt_access(&mut r.jwt);
if let Some(ref mut b) = r.bearer {
redact_jwt_access(&mut b.jwt);
}
}
}
self
}
}
impl ToSql for DefineAccessStatement {
fn fmt_sql(&self, f: &mut String, fmt: SqlFormat) {
let stmt: crate::sql::statements::define::DefineAccessStatement = self.clone().into();
stmt.fmt_sql(f, fmt);
}
}