casbin-rust-cli 1.4.0

casbin-rust-cli is a command-line tool based on Casbin (Rust language), enabling you to use all of Casbin APIs in the shell.
use casbin::{
    prelude::*,
    rhai::{Dynamic, Map},
};
use clap::{CommandFactory, Parser, Subcommand};
use serde_json::{json, Value};
use std::{hash::Hash, str::FromStr, sync::LazyLock};

build_info::build_info!(fn build_info);

static VERSION: LazyLock<String> = LazyLock::new(|| {
    let info = build_info();
    let cli_version = option_env!("VERSION").unwrap_or(env!("CARGO_PKG_VERSION"));
    let casbin_version = info
        .crate_info
        .dependencies
        .iter()
        .find_map(|dep| {
            if dep.name == "casbin" {
                Some(dep.version.to_string())
            } else {
                None
            }
        })
        .expect("casbin version not found");
    format!("{}\ncasbin-rs v{}", cli_version, casbin_version)
});

#[derive(Parser, Debug, Clone)]
#[command(author, about, long_about, version=VERSION.as_str())]
struct Args {
    /// The command to execute
    #[command(subcommand)]
    command: Cmd,
}

#[derive(Subcommand, Debug, Clone)]
#[clap(rename_all = "camelCase")]
pub enum Cmd {
    /// Generate the autocompletion script for the specified shell
    Completion {
        /// The shell to generate the completions for
        #[arg(value_enum)]
        shell: clap_complete_command::Shell,
    },
    /// Check permissions
    Enforce {
        /// The path of the model file or model text
        #[arg(short, long)]
        model: String,

        /// The path of the policy file or policy text
        #[arg(short, long)]
        policy: String,

        /// The arguments for the enforcer
        command_args: Vec<String>,
    },
    /// Check permissions and get which policy it is
    EnforceEx {
        /// The path of the model file or model text
        #[arg(short, long)]
        model: String,

        /// The path of the policy file or policy text
        #[arg(short, long)]
        policy: String,

        /// The arguments for the enforcer
        command_args: Vec<String>,
    },
}

#[tokio::main]
async fn main() {
    let args = Args::parse();

    match args.command {
        Cmd::Enforce {
            model,
            policy,
            command_args,
        } => {
            println!("{}", enforce(&model, &policy, &command_args).await);
        }
        Cmd::EnforceEx {
            model,
            policy,
            command_args,
        } => {
            println!("{}", enforce_ex(&model, &policy, &command_args).await);
        }
        Cmd::Completion { shell } => {
            shell.generate(&mut Args::command(), &mut std::io::stdout());
        }
    };
}

async fn enforce(model: &str, policy: &str, command_args: &[String]) -> String {
    let model = DefaultModel::from_file(model.to_owned())
        .await
        .expect("failed to load model");
    let adapter = FileAdapter::new(policy.to_owned());

    let e = Enforcer::new(model, adapter)
        .await
        .expect("failed to create enforcer");

    let allow = e
        .enforce(parse_args(command_args))
        .expect("failed to enforce");

    json!({
        "allow": allow,
        "explain": Vec::<String>::new(),
    })
    .to_string()
}

async fn enforce_ex(model: &str, policy: &str, command_args: &[String]) -> String {
    let model = DefaultModel::from_file(model.to_owned())
        .await
        .expect("failed to load model");
    let adapter = FileAdapter::new(policy.to_owned());

    let e = Enforcer::new(model, adapter)
        .await
        .expect("failed to create enforcer");

    let (allow, explain) = e
        .enforce_ex(parse_args(command_args))
        .expect("failed to enforce");

    json!({
        "allow": allow,
        "explain": explain.first().unwrap_or(&Vec::<String>::new()),
    })
    .to_string()
}

fn value_to_dynamic(value: Value) -> Dynamic {
    match value {
        Value::Object(map) => {
            let mut rhai_map = Map::new();
            for (k, v) in map {
                let v: Dynamic = value_to_dynamic(v);
                rhai_map.insert(k.into(), v);
            }
            Dynamic::from(rhai_map)
        }
        Value::String(s) => Dynamic::from(s),
        Value::Bool(b) => Dynamic::from(b),
        Value::Number(n) => {
            if n.is_i64() {
                (n.as_i64().unwrap() as i32).into()
            } else if n.is_u64() {
                (n.as_u64().unwrap() as i32).into()
            } else {
                n.as_f64().map(Dynamic::from).unwrap()
            }
        }
        Value::Array(arr) => {
            Dynamic::from(arr.into_iter().map(value_to_dynamic).collect::<Vec<_>>())
        }
        Value::Null => Dynamic::UNIT,
    }
}

#[derive(Clone, Hash, Debug)]
pub struct CommandArg(Value);

impl From<CommandArg> for Dynamic {
    fn from(arg: CommandArg) -> Self {
        value_to_dynamic(arg.0)
    }
}

fn parse_args(command_args: &[String]) -> Vec<CommandArg> {
    command_args
        .iter()
        .map(|arg| {
            CommandArg(
                serde_json::Value::from_str(arg)
                    .unwrap_or(serde_json::Value::String(arg.to_string())),
            )
        })
        .collect::<Vec<_>>()
}

#[cfg(test)]
mod test {
    use super::*;

    #[tokio::test]
    async fn test_enforce() {
        let response = enforce(
            "examples/basic_model.conf",
            "examples/basic_policy.csv",
            &["alice".to_owned(), "data1".to_owned(), "read".to_owned()],
        )
        .await;

        let expected = json!({
            "allow": true,
            "explain": [],
        })
        .to_string();

        assert_eq!(response, expected);
    }

    #[tokio::test]
    async fn test_enforce_explain() {
        let response = enforce_ex(
            "examples/basic_model.conf",
            "examples/basic_policy.csv",
            &["alice".to_owned(), "data1".to_owned(), "read".to_owned()],
        )
        .await;

        let expected = json!({
            "allow": true,
            "explain": &["alice", "data1" ,"read"].iter().map(|s| s.to_string()).collect::<Vec<_>>(),
        })
        .to_string();

        assert_eq!(response, expected);
    }

    #[tokio::test]
    async fn test_abac() {
        let response = enforce_ex(
            "examples/abac_model.conf",
            "examples/abac_policy.csv",
            &["alice".to_owned(), json!({"Owner": "alice"}).to_string()],
        )
        .await;

        let expected = json!({
            "allow": true,
            "explain": [],
        })
        .to_string();

        assert_eq!(response, expected);
    }
}