add auth and dsl crates
This commit is contained in:
parent
1a3fa6242d
commit
8c88695953
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -140,6 +140,16 @@ dependencies = [
|
|||||||
"syn 2.0.101",
|
"syn 2.0.101",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "authorization"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"heromodels",
|
||||||
|
"heromodels_core",
|
||||||
|
"rhai",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "autocfg"
|
name = "autocfg"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@ -2255,9 +2265,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rhai"
|
name = "rhai"
|
||||||
version = "1.22.2"
|
version = "1.21.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2780e813b755850e50b178931aaf94ed24f6817f46aaaf5d21c13c12d939a249"
|
checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash",
|
"ahash",
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
@ -2364,6 +2374,21 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rhailib_dsl"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"authorization",
|
||||||
|
"chrono",
|
||||||
|
"heromodels",
|
||||||
|
"heromodels-derive",
|
||||||
|
"heromodels_core",
|
||||||
|
"rhai",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rhailib_worker"
|
name = "rhailib_worker"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -39,6 +39,6 @@ members = [
|
|||||||
"src/monitor", # Added the new monitor package to workspace
|
"src/monitor", # Added the new monitor package to workspace
|
||||||
"src/repl", # Added the refactored REPL package
|
"src/repl", # Added the refactored REPL package
|
||||||
"examples",
|
"examples",
|
||||||
"src/rhai_engine_ui",
|
"src/rhai_engine_ui", "src/authorization", "src/dsl",
|
||||||
]
|
]
|
||||||
resolver = "2" # Recommended for new workspaces
|
resolver = "2" # Recommended for new workspaces
|
||||||
|
10
src/authorization/Cargo.toml
Normal file
10
src/authorization/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[package]
|
||||||
|
name = "authorization"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rhai = { version = "=1.21.0", features = ["std", "sync", "decimal", "internals"] }
|
||||||
|
heromodels = { path = "../../../db/heromodels" }
|
||||||
|
heromodels_core = { path = "../../../db/heromodels_core" }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
197
src/authorization/src/lib.rs
Normal file
197
src/authorization/src/lib.rs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
//! # Rhai Authorization Crate
|
||||||
|
//! This crate provides authorization mechanisms for Rhai functions, particularly those interacting with a database.
|
||||||
|
//! It includes helper functions for authorization checks and macros to simplify the registration
|
||||||
|
//! of authorized Rhai functions.
|
||||||
|
//! ## Features:
|
||||||
|
//! - `is_super_admin`: Checks if a caller (identified by a public key) is a super admin.
|
||||||
|
//! - `can_access_resource`: Checks if a caller has specific access rights to a resource, using a database connection.
|
||||||
|
//! - `get_caller_public_key`: Helper to extract `CALLER_PUBLIC_KEY` from the Rhai `NativeCallContext`.
|
||||||
|
//! - `id_from_i64_to_u32`: Helper to convert `i64` Rhai IDs to `u32` Rust IDs.
|
||||||
|
//! - `register_authorized_get_by_id_fn!`: Macro to register a Rhai function that retrieves a single item by ID, with authorization checks.
|
||||||
|
//! - `register_authorized_list_fn!`: Macro to register a Rhai function that lists multiple items, filtering them based on authorization.
|
||||||
|
//! ## Usage:
|
||||||
|
//! 1. Use the macros to register your Rhai functions, providing a database connection (`Arc<OurDB>`) and necessary type/name information.
|
||||||
|
//! 2. The macros internally use `can_access_resource` for authorization checks.
|
||||||
|
//! 3. Ensure `CALLER_PUBLIC_KEY` is set in the Rhai engine's scope before calling authorized functions.
|
||||||
|
|
||||||
|
use heromodels::models::access::access::can_access_resource;
|
||||||
|
use heromodels_core::Model;
|
||||||
|
use rhai::{EvalAltResult, NativeCallContext, Position};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
|
/// Extracts the `CALLER_PUBLIC_KEY` string constant from the Rhai `NativeCallContext`.
|
||||||
|
/// This key is used to identify the caller for authorization checks.
|
||||||
|
/// It first checks the current `Scope` and then falls back to the global constants cache.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `context`: The Rhai `NativeCallContext` of the currently executing function.
|
||||||
|
///
|
||||||
|
|
||||||
|
/// Converts an `i64` (common Rhai integer type) to a `u32` (common Rust ID type).
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `id_i64`: The `i64` value to convert.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `Err(EvalAltResult::ErrorMismatchDataType)` if the `i64` value cannot be represented as a `u32`.
|
||||||
|
pub fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>> {
|
||||||
|
u32::try_from(id_i64).map_err(|_|
|
||||||
|
Box::new(EvalAltResult::ErrorMismatchDataType(
|
||||||
|
"u32".to_string(),
|
||||||
|
format!("i64 value ({}) that cannot be represented as u32", id_i64),
|
||||||
|
Position::NONE,
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Extracts the `CALLER_PUBLIC_KEY` string constant from the Rhai `NativeCallContext`'s tag.
|
||||||
|
/// This key is used to identify the caller for authorization checks.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Macro to register a Rhai function that retrieves a single resource by its ID, with authorization.
|
||||||
|
///
|
||||||
|
/// The macro handles:
|
||||||
|
/// - Argument parsing (ID).
|
||||||
|
/// - Caller identification via `CALLER_PUBLIC_KEY`.
|
||||||
|
/// - Authorization check using `AccessControlService::can_access_resource`.
|
||||||
|
/// - Database call to fetch the resource.
|
||||||
|
/// - Error handling for type mismatches, authorization failures, DB errors, and not found errors.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `module`: Mutable reference to the Rhai `Module`.
|
||||||
|
/// * `db_clone`: Cloned `Arc<Db>` for database access.
|
||||||
|
/// * `acs_clone`: Cloned `Arc<AccessControlService>`.
|
||||||
|
/// * `rhai_fn_name`: String literal for the Rhai function name (e.g., "get_collection").
|
||||||
|
/// * `resource_type_str`: String literal for the resource type (e.g., "Collection"), used in authorization checks and error messages.
|
||||||
|
/// * `db_method_name`: Identifier for the database method to call (e.g., `get_by_id`).
|
||||||
|
/// * `id_arg_type`: Rust type of the ID argument in Rhai (e.g., `i64`).
|
||||||
|
/// * `id_rhai_type_name`: String literal for the Rhai type name of the ID (e.g., "i64"), for error messages.
|
||||||
|
/// * `id_conversion_fn`: Path to a function converting `id_arg_type` to `actual_id_type` (e.g., `id_from_i64_to_u32`).
|
||||||
|
/// * `actual_id_type`: Rust type of the ID used in the database (e.g., `u32`).
|
||||||
|
/// * `rhai_return_rust_type`: Rust type of the resource returned by the DB and Rhai function (e.g., `RhaiCollection`).
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! register_authorized_get_by_id_fn {
|
||||||
|
(
|
||||||
|
module: $module:expr,
|
||||||
|
db_clone: $db_clone:expr, // Cloned Arc<OurDB> for database access
|
||||||
|
rhai_fn_name: $rhai_fn_name:expr, // String literal for the Rhai function name (e.g., "get_collection")
|
||||||
|
resource_type_str: $resource_type_str:expr, // String literal for the resource type (e.g., "Collection")
|
||||||
|
rhai_return_rust_type: $rhai_return_rust_type:ty // Rust type of the resource returned (e.g., `RhaiCollection`)
|
||||||
|
) => {
|
||||||
|
let db_instance_auth = $db_clone.clone();
|
||||||
|
let db_instance_fetch = $db_clone.clone();
|
||||||
|
|
||||||
|
FuncRegistration::new($rhai_fn_name).set_into_module(
|
||||||
|
$module,
|
||||||
|
move |context: rhai::NativeCallContext, id_val: i64| -> Result<Option<$rhai_return_rust_type>, Box<EvalAltResult>> {
|
||||||
|
let actual_id: u32 = $crate::id_from_i64_to_u32(id_val)?;
|
||||||
|
|
||||||
|
// Inlined logic to get caller public key
|
||||||
|
let tag_map = context
|
||||||
|
.tag()
|
||||||
|
.and_then(|tag| tag.read_lock::<rhai::Map>())
|
||||||
|
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
|
||||||
|
|
||||||
|
let pk_dynamic = tag_map.get("CALLER_PUBLIC_KEY")
|
||||||
|
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?;
|
||||||
|
|
||||||
|
let caller_pk_str = pk_dynamic.clone().into_string()?;
|
||||||
|
|
||||||
|
// Use the standalone can_access_resource function from heromodels
|
||||||
|
let has_access = heromodels::models::access::access::can_access_resource(
|
||||||
|
db_instance_auth.clone(),
|
||||||
|
&caller_pk_str,
|
||||||
|
actual_id,
|
||||||
|
$resource_type_str,
|
||||||
|
);
|
||||||
|
|
||||||
|
if !has_access {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = db_instance_fetch.get_by_id(actual_id).map_err(|e| {
|
||||||
|
Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
format!("Database error fetching {}: {:?}", $resource_type_str, e).into(),
|
||||||
|
context.position(),
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
Ok(result)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Macro to register a Rhai function that lists all resources of a certain type, with authorization.
|
||||||
|
///
|
||||||
|
/// The macro handles:
|
||||||
|
/// - Caller identification via `CALLER_PUBLIC_KEY`.
|
||||||
|
/// - Fetching all items of a specific type from the database.
|
||||||
|
/// - Filtering the items based on the standalone `can_access_resource` function for each item.
|
||||||
|
/// - Wrapping the authorized items in a specified collection type (e.g., `RhaiCollectionArray`).
|
||||||
|
/// - Error handling for DB errors during fetch or authorization checks.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `module`: Mutable reference to the Rhai `Module`.
|
||||||
|
/// * `db_clone`: Cloned `Arc<OurDB>` for database access.
|
||||||
|
/// * `rhai_fn_name`: String literal for the Rhai function name (e.g., "list_collections").
|
||||||
|
/// * `resource_type_str`: String literal for the resource type (e.g., "Collection"), used in authorization checks.
|
||||||
|
/// * `rhai_return_rust_type`: Rust type of the resource item (e.g., `RhaiCollection`).
|
||||||
|
/// * `item_id_accessor`: Identifier for the method on `rhai_return_rust_type` that returns its ID (e.g., `id`).
|
||||||
|
/// * `rhai_return_wrapper_type`: Rust type that wraps a `Vec` of `rhai_return_rust_type` for Rhai (e.g., `RhaiCollectionArray`).
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! register_authorized_list_fn {
|
||||||
|
(
|
||||||
|
module: $module:expr,
|
||||||
|
db_clone: $db_instance:expr,
|
||||||
|
rhai_fn_name: $rhai_fn_name:expr,
|
||||||
|
resource_type_str: $resource_type_str:expr,
|
||||||
|
rhai_return_rust_type: $rhai_return_rust_type:ty,
|
||||||
|
item_id_accessor: $item_id_accessor:ident,
|
||||||
|
rhai_return_wrapper_type: $rhai_return_wrapper_type:ty
|
||||||
|
) => {
|
||||||
|
let db_instance_auth_outer = $db_instance.clone();
|
||||||
|
let db_instance_fetch = $db_instance.clone();
|
||||||
|
|
||||||
|
FuncRegistration::new($rhai_fn_name).set_into_module(
|
||||||
|
$module,
|
||||||
|
move |context: rhai::NativeCallContext| -> Result<$rhai_return_wrapper_type, Box<EvalAltResult>> {
|
||||||
|
// Inlined logic to get caller public key
|
||||||
|
let tag_map = context
|
||||||
|
.tag()
|
||||||
|
.and_then(|tag| tag.read_lock::<rhai::Map>())
|
||||||
|
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
|
||||||
|
|
||||||
|
let pk_dynamic = tag_map.get("CALLER_PUBLIC_KEY")
|
||||||
|
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?;
|
||||||
|
|
||||||
|
let caller_pk_str = pk_dynamic.clone().into_string()?;
|
||||||
|
|
||||||
|
let all_items: Vec<$rhai_return_rust_type> = db_instance_fetch
|
||||||
|
.collection::<$rhai_return_rust_type>()
|
||||||
|
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("{:?}", e).into(), Position::NONE)))?
|
||||||
|
.get_all()
|
||||||
|
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("{:?}", e).into(), Position::NONE)))?;
|
||||||
|
|
||||||
|
let authorized_items: Vec<$rhai_return_rust_type> = all_items
|
||||||
|
.into_iter()
|
||||||
|
.filter(|item| {
|
||||||
|
let resource_id = item.$item_id_accessor();
|
||||||
|
heromodels::models::access::access::can_access_resource(
|
||||||
|
db_instance_auth_outer.clone(),
|
||||||
|
&caller_pk_str,
|
||||||
|
resource_id,
|
||||||
|
$resource_type_str,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(authorized_items.into())
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
18
src/dsl/Cargo.toml
Normal file
18
src/dsl/Cargo.toml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
[package]
|
||||||
|
name = "rhailib_dsl"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "Central Rhai engine for heromodels"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rhai = { version = "=1.21.0", features = ["std", "sync", "decimal", "internals"] }
|
||||||
|
heromodels = { path = "../../../db/heromodels", features = ["rhai"] }
|
||||||
|
heromodels_core = { path = "../../../db/heromodels_core" }
|
||||||
|
chrono = "0.4"
|
||||||
|
heromodels-derive = { path = "../../../db/heromodels-derive" }
|
||||||
|
authorization = { path = "../authorization"}
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
16
src/dsl/examples/access/access.rhai
Normal file
16
src/dsl/examples/access/access.rhai
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// heromodels/examples/access/access.rhai
|
||||||
|
|
||||||
|
print("--- Testing Access Rhai Module ---");
|
||||||
|
|
||||||
|
// --- Image ---
|
||||||
|
print("\n1. Creating and saving an access...");
|
||||||
|
let access = new_access()
|
||||||
|
.object_id(1)
|
||||||
|
.circle_pk("circle_pk")
|
||||||
|
.group_id(1)
|
||||||
|
.contact_id(1)
|
||||||
|
.expires_at(10);
|
||||||
|
|
||||||
|
save_access(access);
|
||||||
|
|
||||||
|
print("Access saved with ID: " + access.id);
|
39
src/dsl/examples/access/main.rs
Normal file
39
src/dsl/examples/access/main.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
use heromodels::db::hero::OurDB;
|
||||||
|
use rhailib_dsl::access::register_access_rhai_module;
|
||||||
|
use rhai::Engine;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::{fs, path::Path};
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Initialize Rhai engine
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
|
// Initialize database with OurDB
|
||||||
|
let db_path = "temp_access_db";
|
||||||
|
// Clean up previous database file if it exists
|
||||||
|
if Path::new(db_path).exists() {
|
||||||
|
fs::remove_dir_all(db_path)?;
|
||||||
|
}
|
||||||
|
let db = Arc::new(OurDB::new(db_path, true).expect("Failed to create database"));
|
||||||
|
|
||||||
|
// Register the library module with Rhai
|
||||||
|
register_access_rhai_module(&mut engine, db.clone());
|
||||||
|
|
||||||
|
// Load and evaluate the Rhai script
|
||||||
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
||||||
|
let script_path = Path::new(manifest_dir).join("examples").join("access").join("access.rhai");
|
||||||
|
println!("Script path: {}", script_path.display());
|
||||||
|
let script = fs::read_to_string(&script_path)?;
|
||||||
|
|
||||||
|
println!("--- Running Access Rhai Script ---");
|
||||||
|
match engine.eval::<()>(&script) {
|
||||||
|
Ok(_) => println!("\n--- Script executed successfully! ---"),
|
||||||
|
Err(e) => eprintln!("\n--- Script execution failed: {} ---", e),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the database file
|
||||||
|
fs::remove_dir_all(db_path)?;
|
||||||
|
println!("--- Cleaned up temporary database. ---");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
166
src/dsl/examples/library.rs
Normal file
166
src/dsl/examples/library.rs
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
use rhai::{Engine, Module, Position, Scope, Dynamic};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
// Import DB traits with an alias for the Collection trait to avoid naming conflicts.
|
||||||
|
// Import DB traits from heromodels::db as suggested by compiler errors.
|
||||||
|
use heromodels::db::{Db, Collection as DbCollection};
|
||||||
|
use heromodels::{
|
||||||
|
db::hero::OurDB,
|
||||||
|
models::library::collection::Collection, // Actual data model for single items
|
||||||
|
models::library::rhai::RhaiCollectionArray, // Wrapper for arrays of collections
|
||||||
|
models::access::access::Access,
|
||||||
|
};
|
||||||
|
// Import macros and the functions they depend on, which must be in scope during invocation.
|
||||||
|
use rhailib_dsl::{register_authorized_get_by_id_fn, register_authorized_list_fn};
|
||||||
|
|
||||||
|
use rhai::{FuncRegistration, EvalAltResult}; // For macro expansion
|
||||||
|
|
||||||
|
// Rewritten to match the new `Access` model structure.
|
||||||
|
fn grant_access(db: &Arc<OurDB>, user_pk: &str, resource_type: &str, resource_id: u32) {
|
||||||
|
let access_record = Access::new()
|
||||||
|
.circle_pk(user_pk.to_string())
|
||||||
|
.object_type(resource_type.to_string())
|
||||||
|
.object_id(resource_id)
|
||||||
|
.contact_id(0)
|
||||||
|
.group_id(0);
|
||||||
|
|
||||||
|
db.set(&access_record).expect("Failed to set access record");
|
||||||
|
}
|
||||||
|
|
||||||
|
// No changes needed here, but it relies on the new imports to compile.
|
||||||
|
fn register_example_module(engine: &mut Engine, db: Arc<OurDB>) {
|
||||||
|
let mut module = Module::new();
|
||||||
|
|
||||||
|
register_authorized_get_by_id_fn!(
|
||||||
|
module: &mut module,
|
||||||
|
db_clone: db.clone(),
|
||||||
|
rhai_fn_name: "get_collection",
|
||||||
|
resource_type_str: "Collection",
|
||||||
|
rhai_return_rust_type: heromodels::models::library::collection::Collection // Use Collection struct
|
||||||
|
);
|
||||||
|
|
||||||
|
register_authorized_list_fn!(
|
||||||
|
module: &mut module,
|
||||||
|
db_clone: db.clone(),
|
||||||
|
rhai_fn_name: "list_all_collections",
|
||||||
|
resource_type_str: "Collection",
|
||||||
|
rhai_return_rust_type: heromodels::models::library::collection::Collection, // Use Collection struct
|
||||||
|
item_id_accessor: id, // Assumes Collection has an id() method that returns u32
|
||||||
|
rhai_return_wrapper_type: heromodels::models::library::rhai::RhaiCollectionArray // Wrapper type for Rhai
|
||||||
|
);
|
||||||
|
|
||||||
|
engine.register_global_module(module.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
||||||
|
let mut engine = Engine::new();
|
||||||
|
|
||||||
|
let temp_dir = tempdir().unwrap();
|
||||||
|
let db = Arc::new(OurDB::new(temp_dir.path(), false).expect("Failed to create DB"));
|
||||||
|
|
||||||
|
register_example_module(&mut engine, db.clone());
|
||||||
|
|
||||||
|
println!("--- Registered Functions ---");
|
||||||
|
// The closure now returns Option<FuncMetadata> by cloning the metadata.
|
||||||
|
// FuncMetadata is Clone and 'static, satisfying collect_fn_metadata's requirements.
|
||||||
|
for metadata_clone in engine.collect_fn_metadata(None, |info: rhai::FuncInfo<'_>| Some(info.metadata.clone()), true) {
|
||||||
|
if metadata_clone.name == "get_collection" {
|
||||||
|
println!("Found get_collection function, args: {:?}", metadata_clone.param_types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
println!("--------------------------");
|
||||||
|
|
||||||
|
|
||||||
|
// Populate DB using the new `create_collection` helper.
|
||||||
|
// Ownership is no longer on the collection itself, so we don't need owner_pk here.
|
||||||
|
let coll = Collection::new()
|
||||||
|
.title("My new collection")
|
||||||
|
.description("This is a new collection");
|
||||||
|
|
||||||
|
db.set(&coll).expect("Failed to set collection");
|
||||||
|
|
||||||
|
let coll1 = Collection::new()
|
||||||
|
.title("Alice's Private Collection")
|
||||||
|
.description("This is Alice's private collection");
|
||||||
|
let coll2 = Collection::new()
|
||||||
|
.title("Bob's Shared Collection")
|
||||||
|
.description("This is Bob's shared collection");
|
||||||
|
let coll3 = Collection::new()
|
||||||
|
.title("General Collection")
|
||||||
|
.description("This is a general collection");
|
||||||
|
|
||||||
|
db.set(&coll1).expect("Failed to set collection");
|
||||||
|
db.set(&coll2).expect("Failed to set collection");
|
||||||
|
db.set(&coll3).expect("Failed to set collection");
|
||||||
|
|
||||||
|
// Grant access based on the new model.
|
||||||
|
grant_access(&db, "alice_pk", "Collection", coll1.id());
|
||||||
|
grant_access(&db, "bob_pk", "Collection", coll2.id());
|
||||||
|
grant_access(&db, "alice_pk", "Collection", coll2.id()); // Alice can also see Bob's collection.
|
||||||
|
grant_access(&db, "general_user_pk", "Collection", coll3.id());
|
||||||
|
|
||||||
|
println!("--- Rhai Authorization Example ---");
|
||||||
|
|
||||||
|
let mut scope = Scope::new();
|
||||||
|
|
||||||
|
// Scenario 1: Alice accesses her own collection (Success)
|
||||||
|
let mut db_config = rhai::Map::new();
|
||||||
|
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
|
||||||
|
engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
|
||||||
|
|
||||||
|
// Create a Dynamic value holding your DB path or a config object
|
||||||
|
let mut db_config = rhai::Map::new();
|
||||||
|
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
|
||||||
|
db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
|
||||||
|
engine.set_default_tag(Dynamic::from(db_config));
|
||||||
|
|
||||||
|
println!("Alice accessing her collection 1: Success, title"); // Access field directly
|
||||||
|
let result = engine.eval::<Option<Collection>>("get_collection(1)")?;
|
||||||
|
let result_clone = result.clone().expect("REASON");
|
||||||
|
println!("Alice accessing her collection 1: Success, title = {}", result_clone.title); // Access field directly
|
||||||
|
assert_eq!(result_clone.id(), 1);
|
||||||
|
|
||||||
|
// Scenario 2: Bob tries to access Alice's collection (Failure)
|
||||||
|
let mut db_config = rhai::Map::new();
|
||||||
|
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
|
||||||
|
db_config.insert("CALLER_PUBLIC_KEY".into(), "bob_pk".into());
|
||||||
|
engine.set_default_tag(Dynamic::from(db_config));
|
||||||
|
let result = engine.eval_with_scope::<Dynamic>(&mut scope, "get_collection(1)");
|
||||||
|
println!("Bob accessing Alice's collection 1: {:?}", result);
|
||||||
|
let result_clone = result.expect("REASON");
|
||||||
|
println!("Bob accessing Alice's collection 1: {:?}", result_clone);
|
||||||
|
// assert!(result_clone.is_none());
|
||||||
|
|
||||||
|
// Scenario 3: Alice accesses Bob's collection (Success)
|
||||||
|
let mut db_config = rhai::Map::new();
|
||||||
|
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
|
||||||
|
db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
|
||||||
|
engine.set_default_tag(Dynamic::from(db_config));
|
||||||
|
let result = engine.eval_with_scope::<Collection>(&mut scope, "get_collection(2)")?;
|
||||||
|
println!("Alice accessing Bob's collection 2: Success, title = {}", result.title); // Access field directly
|
||||||
|
assert_eq!(result.id(), 2);
|
||||||
|
|
||||||
|
// Scenario 4: General user lists collections (Sees 1)
|
||||||
|
let mut db_config = rhai::Map::new();
|
||||||
|
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
|
||||||
|
db_config.insert("CALLER_PUBLIC_KEY".into(), "general_user_pk".into());
|
||||||
|
engine.set_default_tag(Dynamic::from(db_config));
|
||||||
|
let result = engine.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()").unwrap();
|
||||||
|
println!("General user listing collections: Found {}", result.0.len());
|
||||||
|
assert_eq!(result.0.len(), 1);
|
||||||
|
assert_eq!(result.0[0].id(), 3);
|
||||||
|
|
||||||
|
// Scenario 5: Alice lists collections (Sees 2)
|
||||||
|
let mut db_config = rhai::Map::new();
|
||||||
|
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
|
||||||
|
db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
|
||||||
|
engine.set_default_tag(Dynamic::from(db_config));
|
||||||
|
let collections = engine.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()").unwrap();
|
||||||
|
println!("Alice listing collections: Found {}", collections.0.len());
|
||||||
|
assert_eq!(collections.0.len(), 2);
|
||||||
|
let ids: Vec<u32> = collections.0.iter().map(|c| c.id()).collect();
|
||||||
|
assert!(ids.contains(&1) && ids.contains(&2));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
0
src/dsl/expanded_library.rs
Normal file
0
src/dsl/expanded_library.rs
Normal file
263
src/dsl/src/access.rs
Normal file
263
src/dsl/src/access.rs
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
use heromodels::db::Db;
|
||||||
|
use rhai::plugin::*;
|
||||||
|
use rhai::{Array, Dynamic, Engine, EvalAltResult, INT, Module, Position};
|
||||||
|
use std::mem;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use heromodels::models::access::Access;
|
||||||
|
type RhaiAccess = Access;
|
||||||
|
use heromodels::db::Collection;
|
||||||
|
use heromodels::db::hero::OurDB;
|
||||||
|
|
||||||
|
// Helper to convert i64 from Rhai to u32 for IDs
|
||||||
|
fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>> {
|
||||||
|
u32::try_from(id_i64).map_err(|_| {
|
||||||
|
Box::new(EvalAltResult::ErrorArithmetic(
|
||||||
|
format!("Failed to convert ID '{}' to u32", id_i64).into(),
|
||||||
|
Position::NONE,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[export_module]
|
||||||
|
mod rhai_access_module {
|
||||||
|
// --- Access Functions ---
|
||||||
|
#[rhai_fn(name = "new_access", return_raw)]
|
||||||
|
pub fn new_access() -> Result<RhaiAccess, Box<EvalAltResult>> {
|
||||||
|
let access = Access::new();
|
||||||
|
Ok(access)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the access name
|
||||||
|
#[rhai_fn(name = "object_id", return_raw, global, pure)]
|
||||||
|
pub fn set_object_id(
|
||||||
|
access: &mut RhaiAccess,
|
||||||
|
object_id: i64,
|
||||||
|
) -> Result<RhaiAccess, Box<EvalAltResult>> {
|
||||||
|
let id = id_from_i64_to_u32(object_id)?;
|
||||||
|
let owned_access = std::mem::take(access);
|
||||||
|
*access = owned_access.object_id(id);
|
||||||
|
Ok(access.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the access name
|
||||||
|
#[rhai_fn(name = "circle_pk", return_raw, global, pure)]
|
||||||
|
pub fn set_circle_pk(
|
||||||
|
access: &mut RhaiAccess,
|
||||||
|
circle_pk: String,
|
||||||
|
) -> Result<RhaiAccess, Box<EvalAltResult>> {
|
||||||
|
let owned_access = std::mem::take(access);
|
||||||
|
*access = owned_access.circle_pk(circle_pk);
|
||||||
|
Ok(access.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the access name
|
||||||
|
#[rhai_fn(name = "group_id", return_raw, global, pure)]
|
||||||
|
pub fn set_group_id(
|
||||||
|
access: &mut RhaiAccess,
|
||||||
|
group_id: i64,
|
||||||
|
) -> Result<RhaiAccess, Box<EvalAltResult>> {
|
||||||
|
let id = id_from_i64_to_u32(group_id)?;
|
||||||
|
let owned_access = std::mem::take(access);
|
||||||
|
*access = owned_access.group_id(id);
|
||||||
|
Ok(access.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rhai_fn(name = "contact_id", return_raw, global, pure)]
|
||||||
|
pub fn set_contact_id(
|
||||||
|
access: &mut RhaiAccess,
|
||||||
|
contact_id: i64,
|
||||||
|
) -> Result<RhaiAccess, Box<EvalAltResult>> {
|
||||||
|
let id = id_from_i64_to_u32(contact_id)?;
|
||||||
|
let owned_access = std::mem::take(access);
|
||||||
|
*access = owned_access.contact_id(id);
|
||||||
|
Ok(access.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rhai_fn(name = "expires_at", return_raw, global, pure)]
|
||||||
|
pub fn set_expires_at(
|
||||||
|
access: &mut RhaiAccess,
|
||||||
|
expires_at: i64,
|
||||||
|
) -> Result<RhaiAccess, Box<EvalAltResult>> {
|
||||||
|
let expires_at = expires_at as u64;
|
||||||
|
let owned_access = std::mem::take(access);
|
||||||
|
*access = owned_access.expires_at(Some(expires_at));
|
||||||
|
Ok(access.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access Getters
|
||||||
|
#[rhai_fn(get = "id", pure)]
|
||||||
|
pub fn get_access_id(access: &mut RhaiAccess) -> i64 {
|
||||||
|
access.base_data.id as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rhai_fn(get = "object_id", pure)]
|
||||||
|
pub fn get_access_object_id(access: &mut RhaiAccess) -> i64 {
|
||||||
|
access.object_id as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rhai_fn(get = "circle_pk", pure)]
|
||||||
|
pub fn get_access_circle_pk(access: &mut RhaiAccess) -> String {
|
||||||
|
access.circle_pk.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rhai_fn(get = "group_id", pure)]
|
||||||
|
pub fn get_access_group_id(access: &mut RhaiAccess) -> i64 {
|
||||||
|
access.group_id as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rhai_fn(get = "contact_id", pure)]
|
||||||
|
pub fn get_access_contact_id(access: &mut RhaiAccess) -> i64 {
|
||||||
|
access.contact_id as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rhai_fn(get = "expires_at", pure)]
|
||||||
|
pub fn get_access_expires_at(access: &mut RhaiAccess) -> i64 {
|
||||||
|
access.expires_at.unwrap_or(0) as i64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rhai_fn(get = "created_at", pure)]
|
||||||
|
pub fn get_access_created_at(access: &mut RhaiAccess) -> i64 {
|
||||||
|
access.base_data.created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rhai_fn(get = "modified_at", pure)]
|
||||||
|
pub fn get_access_modified_at(access: &mut RhaiAccess) -> i64 {
|
||||||
|
access.base_data.modified_at
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// // A function that takes the call context and an integer argument.
|
||||||
|
// fn save_access(context: NativeCallContext, access: RhaiAccess) -> Result<(), Box<EvalAltResult>> {
|
||||||
|
// let optional_tag_ref: Option<&Dynamic> = context.tag();
|
||||||
|
|
||||||
|
// // Ensure the tag exists
|
||||||
|
// let tag_ref: &Dynamic = optional_tag_ref.ok_or_else(|| {
|
||||||
|
// Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
// "Custom tag not set for this evaluation run.".into(),
|
||||||
|
// context.position(), // Use context.position() if available and relevant
|
||||||
|
// ))
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// // Initialize database with OurDB for the Rhai engine
|
||||||
|
// // Using a temporary/in-memory like database for the worker
|
||||||
|
// let tag_map = tag_ref.read_lock::<rhai::Map>().ok_or_else(|| {
|
||||||
|
// Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
// "Tag is not a Map or is locked".into(),
|
||||||
|
// Position::NONE,
|
||||||
|
// ))
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// let db_path = tag_map.get("CIRCLE_DB_PATH").expect("CIRCLE_DB_PATH not found").as_str().to_string();
|
||||||
|
// let db = Arc::new(
|
||||||
|
// OurDB::new(db_path, false)
|
||||||
|
// .expect("Failed to create temporary DB for Rhai engine"),
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let result = db.set(&access).map_err(|e| {
|
||||||
|
// Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
// format!("DB Error set_access: {}", e).into(),
|
||||||
|
// Position::NONE,
|
||||||
|
// ))
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// // Return the updated access with the correct ID
|
||||||
|
// Ok(result)
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
pub fn register_access_rhai_module(engine: &mut Engine, db: Arc<OurDB>) {
|
||||||
|
// Register the exported module globally
|
||||||
|
let module = exported_module!(rhai_access_module);
|
||||||
|
engine.register_global_module(module.into());
|
||||||
|
|
||||||
|
// Create a module for database functions
|
||||||
|
let mut db_module = Module::new();
|
||||||
|
|
||||||
|
// let db_clone_set_access = db.clone();
|
||||||
|
// db_module.set_native_fn(
|
||||||
|
// "save_access",
|
||||||
|
// move |access: Access| -> Result<Access, Box<EvalAltResult>> {
|
||||||
|
// // Use the Collection trait method directly
|
||||||
|
// let result = db_clone_set_access.set(&access).map_err(|e| {
|
||||||
|
// Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
// format!("DB Error set_access: {}", e).into(),
|
||||||
|
// Position::NONE,
|
||||||
|
// ))
|
||||||
|
// })?;
|
||||||
|
|
||||||
|
// // Return the updated access with the correct ID
|
||||||
|
// Ok(result.1)
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Manually register database functions as they need to capture 'db'
|
||||||
|
let db_clone_delete_access = db.clone();
|
||||||
|
db_module.set_native_fn(
|
||||||
|
"delete_access",
|
||||||
|
move |access: Access| -> Result<(), Box<EvalAltResult>> {
|
||||||
|
// Use the Collection trait method directly
|
||||||
|
let result = db_clone_delete_access
|
||||||
|
.collection::<Access>()
|
||||||
|
.expect("can open access collection")
|
||||||
|
.delete_by_id(access.base_data.id)
|
||||||
|
.expect("can delete event");
|
||||||
|
|
||||||
|
// Return the updated event with the correct ID
|
||||||
|
Ok(result)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let db_clone_get_access = db.clone();
|
||||||
|
db_module.set_native_fn(
|
||||||
|
"get_access_by_id",
|
||||||
|
move |id_i64: INT| -> Result<Access, Box<EvalAltResult>> {
|
||||||
|
let id_u32 = id_from_i64_to_u32(id_i64)?;
|
||||||
|
// Use the Collection trait method directly
|
||||||
|
db_clone_get_access
|
||||||
|
.get_by_id(id_u32)
|
||||||
|
.map_err(|e| {
|
||||||
|
Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
format!("DB Error get_access_by_id: {}", e).into(),
|
||||||
|
Position::NONE,
|
||||||
|
))
|
||||||
|
})?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
format!("Access with ID {} not found", id_u32).into(),
|
||||||
|
Position::NONE,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add list_accesss function to get all accesss
|
||||||
|
let db_clone_list_accesss = db.clone();
|
||||||
|
db_module.set_native_fn(
|
||||||
|
"list_accesss",
|
||||||
|
move || -> Result<Dynamic, Box<EvalAltResult>> {
|
||||||
|
let collection = db_clone_list_accesss.collection::<Access>().map_err(|e| {
|
||||||
|
Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
format!("Failed to get access collection: {:?}", e).into(),
|
||||||
|
Position::NONE,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let accesss = collection.get_all().map_err(|e| {
|
||||||
|
Box::new(EvalAltResult::ErrorRuntime(
|
||||||
|
format!("Failed to get all accesss: {:?}", e).into(),
|
||||||
|
Position::NONE,
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let mut array = Array::new();
|
||||||
|
for access in accesss {
|
||||||
|
array.push(Dynamic::from(access));
|
||||||
|
}
|
||||||
|
Ok(Dynamic::from(array))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register the database module globally
|
||||||
|
engine.register_global_module(db_module.into());
|
||||||
|
|
||||||
|
println!("Successfully registered access Rhai module using export_module approach.");
|
||||||
|
}
|
6
src/dsl/src/lib.rs
Normal file
6
src/dsl/src/lib.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
pub mod library;
|
||||||
|
pub mod access;
|
||||||
|
|
||||||
|
pub use authorization::register_authorized_get_by_id_fn;
|
||||||
|
pub use authorization::register_authorized_list_fn;
|
||||||
|
pub use authorization::id_from_i64_to_u32;
|
1031
src/dsl/src/library.rs
Normal file
1031
src/dsl/src/library.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -65,7 +65,7 @@ fn seed_calendar_data(db: Arc<OurDB>) {
|
|||||||
calendar.description = Some("My work schedule".to_string());
|
calendar.description = Some("My work schedule".to_string());
|
||||||
|
|
||||||
// Store the calendar in the database
|
// Store the calendar in the database
|
||||||
let (calendar_id, updated_calendar) = db
|
let (_calendar_id, _updated_calendar) = db
|
||||||
.collection::<Calendar>()
|
.collection::<Calendar>()
|
||||||
.expect("Failed to get Calendar collection")
|
.expect("Failed to get Calendar collection")
|
||||||
.set(&calendar)
|
.set(&calendar)
|
||||||
@ -107,7 +107,7 @@ fn seed_calendar_data(db: Arc<OurDB>) {
|
|||||||
calendar = calendar.add_event(event_id as i64);
|
calendar = calendar.add_event(event_id as i64);
|
||||||
|
|
||||||
// Store the calendar in the database
|
// Store the calendar in the database
|
||||||
let (calendar_id, updated_calendar) = db
|
let (_calendar_id, updated_calendar) = db
|
||||||
.collection::<Calendar>()
|
.collection::<Calendar>()
|
||||||
.expect("Failed to get Calendar collection")
|
.expect("Failed to get Calendar collection")
|
||||||
.set(&calendar)
|
.set(&calendar)
|
||||||
@ -348,7 +348,7 @@ fn seed_finance_data(db: Arc<OurDB>) {
|
|||||||
.add_tag("collectible".to_string());
|
.add_tag("collectible".to_string());
|
||||||
|
|
||||||
// Store the listing in the database
|
// Store the listing in the database
|
||||||
let (listing_id, updated_listing) = db
|
let (_listing_id, updated_listing) = db
|
||||||
.collection::<Listing>()
|
.collection::<Listing>()
|
||||||
.expect("Failed to get Listing collection")
|
.expect("Failed to get Listing collection")
|
||||||
.set(&listing)
|
.set(&listing)
|
||||||
|
Loading…
Reference in New Issue
Block a user