add auth and dsl crates
This commit is contained in:
		
							
								
								
									
										29
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										29
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -140,6 +140,16 @@ dependencies = [ | ||||
|  "syn 2.0.101", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "authorization" | ||||
| version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "heromodels", | ||||
|  "heromodels_core", | ||||
|  "rhai", | ||||
|  "serde", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "autocfg" | ||||
| version = "1.4.0" | ||||
| @@ -2255,9 +2265,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" | ||||
|  | ||||
| [[package]] | ||||
| name = "rhai" | ||||
| version = "1.22.2" | ||||
| version = "1.21.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "2780e813b755850e50b178931aaf94ed24f6817f46aaaf5d21c13c12d939a249" | ||||
| checksum = "ce4d759a4729a655ddfdbb3ff6e77fb9eadd902dae12319455557796e435d2a6" | ||||
| dependencies = [ | ||||
|  "ahash", | ||||
|  "bitflags 2.9.1", | ||||
| @@ -2364,6 +2374,21 @@ dependencies = [ | ||||
|  "tokio", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "rhailib_dsl" | ||||
| version = "0.1.0" | ||||
| dependencies = [ | ||||
|  "authorization", | ||||
|  "chrono", | ||||
|  "heromodels", | ||||
|  "heromodels-derive", | ||||
|  "heromodels_core", | ||||
|  "rhai", | ||||
|  "serde", | ||||
|  "serde_json", | ||||
|  "tempfile", | ||||
| ] | ||||
|  | ||||
| [[package]] | ||||
| name = "rhailib_worker" | ||||
| version = "0.1.0" | ||||
|   | ||||
| @@ -39,6 +39,6 @@ members = [ | ||||
|     "src/monitor", # Added the new monitor package to workspace | ||||
|     "src/repl", # Added the refactored REPL package | ||||
|     "examples", | ||||
|     "src/rhai_engine_ui", | ||||
|     "src/rhai_engine_ui", "src/authorization", "src/dsl", | ||||
| ] | ||||
| 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()); | ||||
|  | ||||
|     // Store the calendar in the database | ||||
|     let (calendar_id, updated_calendar) = db | ||||
|     let (_calendar_id, _updated_calendar) = db | ||||
|         .collection::<Calendar>() | ||||
|         .expect("Failed to get Calendar collection") | ||||
|         .set(&calendar) | ||||
| @@ -107,7 +107,7 @@ fn seed_calendar_data(db: Arc<OurDB>) { | ||||
|     calendar = calendar.add_event(event_id as i64); | ||||
|  | ||||
|     // Store the calendar in the database | ||||
|     let (calendar_id, updated_calendar) = db | ||||
|     let (_calendar_id, updated_calendar) = db | ||||
|         .collection::<Calendar>() | ||||
|         .expect("Failed to get Calendar collection") | ||||
|         .set(&calendar) | ||||
| @@ -348,7 +348,7 @@ fn seed_finance_data(db: Arc<OurDB>) { | ||||
|         .add_tag("collectible".to_string()); | ||||
|  | ||||
|     // Store the listing in the database | ||||
|     let (listing_id, updated_listing) = db | ||||
|     let (_listing_id, updated_listing) = db | ||||
|         .collection::<Listing>() | ||||
|         .expect("Failed to get Listing collection") | ||||
|         .set(&listing) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user