use super::EngineResult;
use crate::{error::RuntimeError, get_global};
pub use auth::*;
use rhai::plugin::{
Dynamic, FnAccess, FnNamespace, Module, NativeCallContext, PluginFunction, RhaiResult, TypeId,
};
use vsmtp_common::{auth::Credentials, status::Status, FieldAccessError, Stage};
use crate::api::state;
#[rhai::plugin::export_module]
mod auth {
#[rhai_fn(name = "unix_users", return_raw)]
pub fn unix_users(ncc: NativeCallContext) -> EngineResult<Status> {
let ctx = get_global!(ncc, ctx);
let ctx = vsl_guard_ok!(ctx.read());
match &ctx
.auth()
.as_ref()
.ok_or_else(|| {
RuntimeError::MissingField(FieldAccessError::new(
"auth",
vec![
Stage::Connect,
Stage::Helo,
Stage::MailFrom,
Stage::RcptTo,
Stage::Finished,
],
))
})
.expect("state cannot be empty")
.credentials
{
Some(Credentials::Verify { authid, authpass }) => {
super::execute_testsaslauthd(authid, authpass)
}
Some(Credentials::AnonymousToken { token }) => {
tracing::warn!("Cannot authenticate unix user with an anonymous token");
tracing::trace!(token);
Ok(state::deny())
}
None => {
tracing::warn!("No credentials found to authenticate a unix user with");
Ok(state::deny())
}
}
}
#[rhai_fn(name = "is_authenticated", return_raw)]
pub fn is_authenticated(ncc: NativeCallContext) -> EngineResult<bool> {
Ok(vsl_guard_ok!(get_global!(ncc, ctx).read()).auth().is_some())
}
#[rhai_fn(name = "credentials", return_raw)]
pub fn credentials(ncc: NativeCallContext) -> EngineResult<Credentials> {
vsl_guard_ok!(get_global!(ncc, ctx).read())
.auth()
.as_ref()
.and_then(|auth| auth.credentials.clone())
.map_or_else(
|| {
Err("no `credentials` available in the connection"
.to_string()
.into())
},
Ok,
)
}
#[rhai_fn(global, get = "type", pure)]
pub fn get_type(credentials: &mut Credentials) -> String {
credentials.to_string()
}
#[rhai_fn(global, get = "authid", return_raw, pure)]
pub fn get_authid(credentials: &mut Credentials) -> EngineResult<String> {
match credentials {
Credentials::Verify { authid, .. } => Ok(authid.clone()),
Credentials::AnonymousToken { .. } => {
Err(format!("no `authid` available in credentials of type `{credentials}`").into())
}
}
}
#[rhai_fn(global, get = "authpass", return_raw, pure)]
pub fn get_authpass(credentials: &mut Credentials) -> EngineResult<String> {
match credentials {
Credentials::Verify { authpass, .. } => Ok(authpass.clone()),
Credentials::AnonymousToken { .. } => Err(format!(
"no `authpass` available in credentials of type `{credentials}`"
)
.into()),
}
}
#[rhai_fn(global, get = "anonymous_token", return_raw, pure)]
pub fn get_anonymous_token(credentials: &mut Credentials) -> EngineResult<String> {
match credentials {
Credentials::AnonymousToken { token } => Ok(token.clone()),
Credentials::Verify { .. } => Err(format!(
"no `anonymous_token` available in credentials of type `{credentials}`"
)
.into()),
}
}
}
fn execute_testsaslauthd(authid: &str, authpass: &str) -> EngineResult<Status> {
let testsaslauthd = rhai::Shared::new(crate::dsl::cmd::service::Cmd {
timeout: std::time::Duration::from_secs(1),
user: None,
group: None,
command: "testsaslauthd".to_string(),
args: Some(
["-u", authid, "-p", authpass]
.into_iter()
.map(std::borrow::ToOwned::to_owned)
.collect::<Vec<_>>(),
),
});
let result = vsl_generic_ok!(testsaslauthd.run());
if let Some(signal) = std::os::unix::prelude::ExitStatusExt::signal(&result) {
tracing::warn!(
signal = signal.to_string(),
"authentication command received a signal"
);
return Ok(state::deny());
}
#[allow(clippy::option_if_let_else)]
Ok(match result.code() {
Some(code) if code == 0 => state::accept(),
_ => state::deny(),
})
}