vsmtp-rule-engine 2.0.1-rc.4

Next-gen MTA. Secured, Faster and Greener
Documentation
/*
 * vSMTP mail transfer agent
 * Copyright (C) 2022 viridIT SAS
 *
 * This program is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free Software
 * Foundation, either version 3 of the License, or any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program. If not, see https://www.gnu.org/licenses/.
 *
*/

use rhai::plugin::{
    Dynamic, FnAccess, FnNamespace, Module, NativeCallContext, PluginFunction, RhaiResult, TypeId,
};

pub use auth::*;
use vsmtp_common::status::Status;

use crate::api::state;

use super::EngineResult;

/// Authentication mechanisms and credential manipulation.
#[rhai::plugin::export_module]
mod auth {
    use crate::api::state;
    use crate::api::EngineResult;
    use crate::{get_global, ExecutionStage};
    use vsmtp_common::{auth::Credentials, status::Status};

    /// Process the SASL authentication mechanism.
    ///
    /// The current implementation support "PLAIN" mechanism, and will call the
    /// `testsaslauthd` program to check the credentials.
    ///
    /// The credentials will be verified depending on the mode of `saslauthd`.
    ///
    /// A native implementation will be provided in the future.
    #[allow(clippy::needless_pass_by_value)]
    #[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()
            .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())
            }
        }
    }

    /// Check if the client is authenticated.
    ///
    /// # Effective smtp stage
    ///
    /// `authenticate` stage only.
    ///
    /// # Return
    ///
    /// * `bool` - `true` if the client succeeded to authenticate itself, `false` otherwise.
    ///
    /// # Example
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     authenticate: [
    ///        action "log info" || log("info", `client authenticated: ${auth::is_authenticated()}`),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[allow(clippy::needless_pass_by_value)]
    #[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())
    }

    /// Get authentication credentials from the client.
    ///
    /// # Effective smtp stage
    ///
    /// `authenticate` only.
    ///
    /// # Return
    ///
    /// * `Credentials` - the credentials of the client.
    ///
    /// # Example
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     authenticate: [
    ///        action "log auth" || log("info", `${auth::credentials()}`),
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[allow(clippy::needless_pass_by_value)]
    #[rhai_fn(name = "credentials", return_raw)]
    pub fn credentials(ncc: NativeCallContext) -> EngineResult<Credentials> {
        Ok(vsl_missing_ok!(
            vsl_missing_ok!(
                vsl_guard_ok!(get_global!(ncc, ctx)?.read()).auth(),
                "auth",
                ExecutionStage::Authenticate
            )
            .credentials,
            "credentials",
            ExecutionStage::Authenticate
        )
        .clone())
    }

    /// Get the type of the `auth` property of the connection.
    ///
    /// # Effective smtp stage
    ///
    /// `authenticate` only.
    ///
    /// # Return
    ///
    /// * `String` - the credentials type.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     authenticate: [
    ///        action "log auth type" || {
    ///             let credentials = auth::credentials();
    ///
    ///             // Logs here will output 'Verify' or 'AnonymousToken'.
    ///             // depending on the authentication type.
    ///             log("info", `credentials type: ${credentials.type}`);
    ///         },
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[rhai_fn(global, get = "type", pure)]
    pub fn get_type(credentials: &mut Credentials) -> String {
        credentials.to_string()
    }

    /// Get the `authid` property of the connection.
    /// Can only be use on 'Verify' authentication typed credentials.
    ///
    /// # Effective smtp stage
    ///
    /// `authenticate` only.
    ///
    /// # Return
    ///
    /// * `String` - the authentication id.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     authenticate: [
    ///        action "log auth id" || {
    ///             let credentials = auth::credentials();
    ///             log("info", `credentials id: ${credentials.authid}`);
    ///         },
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[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())
            }
        }
    }

    /// Get the `authpass` property of the connection.
    /// Can only be use on 'Verify' authentication typed credentials.
    ///
    /// # Effective smtp stage
    ///
    /// `authenticate` only.
    ///
    /// # Return
    ///
    /// * `String` - the authentication password.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     authenticate: [
    ///        action "log auth pass" || {
    ///             let credentials = auth::credentials();
    ///             log("info", `credentials pass: ${credentials.authpass}`);
    ///         },
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[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()),
        }
    }

    /// Get the `anonymous_token` property of the connection.
    /// Can only be use on 'AnonymousToken' authentication typed credentials.
    ///
    /// # Effective smtp stage
    ///
    /// `authenticate` only.
    ///
    /// # Return
    ///
    /// * `String` - the token.
    ///
    /// # Examples
    ///
    /// ```
    /// # vsmtp_test::vsl::run(
    /// # |builder| Ok(builder.add_root_filter_rules(r#"
    /// #{
    ///     authenticate: [
    ///        action "log auth token" || {
    ///             let credentials = auth::credentials();
    ///             log("info", `credentials token: ${credentials.anonymous_token}`);
    ///         },
    ///     ]
    /// }
    /// # "#)?.build()));
    /// ```
    #[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(),
    })
}