blob: 221fcdad61de24ef45f51acf50d34abc3db10f40 [file] [log] [blame]
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);
}
}