diff --git a/Cargo.lock b/Cargo.lock index cf98cbf..4f36fbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -140,16 +140,6 @@ 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" @@ -1653,6 +1643,16 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "macros" +version = "0.1.0" +dependencies = [ + "heromodels", + "heromodels_core", + "rhai", + "serde", +] + [[package]] name = "matchers" version = "0.1.0" @@ -2378,11 +2378,11 @@ dependencies = [ name = "rhailib_dsl" version = "0.1.0" dependencies = [ - "authorization", "chrono", "heromodels", "heromodels-derive", "heromodels_core", + "macros", "rhai", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index b7f06ef..e777d2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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/authorization", "src/dsl", + "src/rhai_engine_ui", "src/macros", "src/dsl", ] resolver = "2" # Recommended for new workspaces diff --git a/src/dsl/Cargo.toml b/src/dsl/Cargo.toml index c2905a0..5757462 100644 --- a/src/dsl/Cargo.toml +++ b/src/dsl/Cargo.toml @@ -10,7 +10,7 @@ heromodels = { path = "../../../db/heromodels", features = ["rhai"] } heromodels_core = { path = "../../../db/heromodels_core" } chrono = "0.4" heromodels-derive = { path = "../../../db/heromodels-derive" } -authorization = { path = "../authorization"} +macros = { path = "../macros"} serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/src/dsl/examples/library.rs b/src/dsl/examples/library.rs index 7192558..c8d3392 100644 --- a/src/dsl/examples/library.rs +++ b/src/dsl/examples/library.rs @@ -34,7 +34,6 @@ fn register_example_module(engine: &mut Engine, db: Arc) { 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 @@ -53,25 +52,12 @@ fn register_example_module(engine: &mut Engine, db: Arc) { engine.register_global_module(module.into()); } -fn main() -> Result<(), Box> { +fn create_alice_engine(db_dir: &str, alice_pk: &str) -> Engine { 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 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!("--------------------------"); - - + let db_path = format!("{}/{}", db_dir, alice_pk); + let db = Arc::new(OurDB::new(&db_path, false).expect("Failed to create DB")); + // 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() @@ -83,70 +69,111 @@ fn main() -> Result<(), Box> { 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()); + grant_access(&db, "user_pk", "Collection", coll3.id()); + + register_example_module(&mut engine, db.clone()); + let mut db_config = rhai::Map::new(); + db_config.insert("DB_PATH".into(), db_dir.clone().into()); + db_config.insert("CIRCLE_PUBLIC_KEY".into(), "alice_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + engine +} +fn create_bob_engine(db_dir: &str, bob_pk: &str) -> Engine { + let mut engine = Engine::new(); + + let db_path = format!("{}/{}", db_dir, bob_pk); + let db = Arc::new(OurDB::new(db_path, false).expect("Failed to create DB")); + + let coll2 = Collection::new() + .title("Bob's Shared Collection") + .description("This is Bob's shared collection Alice has access."); + + db.set(&coll2).expect("Failed to set collection"); + grant_access(&db, "alice_pk", "Collection", coll2.id()); + + register_example_module(&mut engine, db.clone()); + let mut db_config = rhai::Map::new(); + db_config.insert("DB_PATH".into(), db_dir.clone().into()); + db_config.insert("CIRCLE_PUBLIC_KEY".into(), "bob_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + engine +} + +fn create_user_engine(db_dir: &str, user_pk: &str) -> Engine { + let mut engine = Engine::new(); + + let db_path = format!("{}/{}", db_dir, user_pk); + let db = Arc::new(OurDB::new(db_path, false).expect("Failed to create DB")); + register_example_module(&mut engine, db.clone()); + let mut db_config = rhai::Map::new(); + db_config.insert("DB_PATH".into(), db_dir.clone().into()); + db_config.insert("CIRCLE_PUBLIC_KEY".into(), "user_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + engine +} + +fn main() -> Result<(), Box> { + let db_path = format!("{}/hero/db", std::env::var("HOME").unwrap()); + let alice_pk = "alice_pk"; + let bob_pk = "bob_pk"; + let user_pk = "user_pk"; + + let mut engine_alice = create_alice_engine(&db_path, alice_pk); + let mut engine_bob = create_bob_engine(&db_path, bob_pk); + let mut engine_user = create_user_engine(&db_path, user_pk); + + println!("--------------------------"); 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)); + { + let mut tag_dynamic = engine_alice.default_tag_mut().as_map_mut().unwrap(); + tag_dynamic.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into()); + } + // engine_alice.set_default_tag(Dynamic::from(tag_dynamic.clone())); println!("Alice accessing her collection 1: Success, title"); // Access field directly - let result = engine.eval::>("get_collection(1)")?; - let result_clone = result.clone().expect("REASON"); + let result = engine_alice.eval::>("get_collection(1)")?; + let result_clone = result.clone().expect("Failed to retrieve collection. It might not exist or you may not have access."); 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::(&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()); + { + let mut tag_dynamic = engine_bob.default_tag_mut().as_map_mut().unwrap(); + tag_dynamic.insert("CALLER_PUBLIC_KEY".into(), "bob_pk".into()); + } + let result = engine_alice.eval_with_scope::>(&mut scope, "get_collection(1)")?; + println!("Bob accessing Alice's collection 1: Failure as expected ({:?})", result); + assert!(result.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::(&mut scope, "get_collection(2)")?; - println!("Alice accessing Bob's collection 2: Success, title = {}", result.title); // Access field directly - assert_eq!(result.id(), 2); + engine_bob.set_default_tag(Dynamic::from(db_config)); + let result: Option = engine_bob.eval_with_scope::>(&mut scope, "get_collection(2)")?; + let collection = result.expect("Alice should have access to Bob's collection"); + println!("Alice accessing Bob's collection 2: Success, title = {}", collection.title); // Access field directly + assert_eq!(collection.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::(&mut scope, "list_all_collections()").unwrap(); + engine_user.set_default_tag(Dynamic::from(db_config)); + let result = engine_user.eval_with_scope::(&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); @@ -155,8 +182,8 @@ fn main() -> Result<(), Box> { 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::(&mut scope, "list_all_collections()").unwrap(); + engine_alice.set_default_tag(Dynamic::from(db_config)); + let collections = engine_alice.eval_with_scope::(&mut scope, "list_all_collections()").unwrap(); println!("Alice listing collections: Found {}", collections.0.len()); assert_eq!(collections.0.len(), 2); let ids: Vec = collections.0.iter().map(|c| c.id()).collect(); diff --git a/src/dsl/expanded_library.rs b/src/dsl/expanded_library.rs index e69de29..4ced9fc 100644 --- a/src/dsl/expanded_library.rs +++ b/src/dsl/expanded_library.rs @@ -0,0 +1,395 @@ +#![feature(prelude_import)] +#[prelude_import] +use std::prelude::rust_2021::*; +#[macro_use] +extern crate std; +use rhai::{Engine, Module, Position, Scope, Dynamic}; +use std::sync::Arc; +use tempfile::tempdir; +use heromodels::db::{Db, Collection as DbCollection}; +use heromodels::{ + db::hero::OurDB, models::library::collection::Collection, + models::library::rhai::RhaiCollectionArray, models::access::access::Access, +}; +use rhailib_dsl::{register_authorized_get_by_id_fn, register_authorized_list_fn}; +use rhai::{FuncRegistration, EvalAltResult}; +fn grant_access(db: &Arc, 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"); +} +fn register_example_module(engine: &mut Engine, db: Arc) { + let mut module = Module::new(); + FuncRegistration::new("get_collection") + .set_into_module( + &mut module, + move | + context: rhai::NativeCallContext, + id_val: i64, + | -> Result< + Option, + Box, + > { + let actual_id: u32 = ::macros::id_from_i64_to_u32(id_val)?; + let tag_map = context + .tag() + .and_then(|tag| tag.read_lock::()) + .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 circle_pk = tag_map + .get("CIRCLE_PUBLIC_KEY") + .ok_or_else(|| Box::new( + EvalAltResult::ErrorRuntime( + "'CIRCLE_PUBLIC_KEY' not found in context tag Map.".into(), + context.position(), + ), + ))?; + let circle_pk = circle_pk.clone().into_string()?; + let db_path = ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!("~/hero/{0}", circle_pk), + ); + res + }); + let db = Arc::new( + OurDB::new(db_path, false).expect("Failed to create DB"), + ); + let caller_pk_str = pk_dynamic.clone().into_string()?; + { + ::std::io::_print( + format_args!( + "Checking access for public key: {0}\n", + caller_pk_str, + ), + ); + }; + if circle_pk != caller_pk_str { + let has_access = heromodels::models::access::access::can_access_resource( + db.clone(), + &caller_pk_str, + actual_id, + "Collection", + ); + if !has_access { + return Ok(None); + } + } + let result = db + .get_by_id(actual_id) + .map_err(|e| { + Box::new( + EvalAltResult::ErrorRuntime( + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format( + format_args!( + "Database error fetching {0}: {1:?}", + "Collection", + e, + ), + ); + res + }) + .into(), + context.position(), + ), + ) + })?; + Ok(result) + }, + ); + let db_instance_auth_outer = db.clone().clone(); + let db_instance_fetch = db.clone().clone(); + FuncRegistration::new("list_all_collections") + .set_into_module( + &mut module, + move | + context: rhai::NativeCallContext, + | -> Result< + heromodels::models::library::rhai::RhaiCollectionArray, + Box, + > { + let tag_map = context + .tag() + .and_then(|tag| tag.read_lock::()) + .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< + heromodels::models::library::collection::Collection, + > = db_instance_fetch + .collection::() + .map_err(|e| Box::new( + EvalAltResult::ErrorRuntime( + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format(format_args!("{0:?}", e)); + res + }) + .into(), + Position::NONE, + ), + ))? + .get_all() + .map_err(|e| Box::new( + EvalAltResult::ErrorRuntime( + ::alloc::__export::must_use({ + let res = ::alloc::fmt::format(format_args!("{0:?}", e)); + res + }) + .into(), + Position::NONE, + ), + ))?; + let authorized_items: Vec< + heromodels::models::library::collection::Collection, + > = all_items + .into_iter() + .filter(|item| { + let resource_id = item.id(); + heromodels::models::access::access::can_access_resource( + db_instance_auth_outer.clone(), + &caller_pk_str, + resource_id, + "Collection", + ) + }) + .collect(); + Ok(authorized_items.into()) + }, + ); + engine.register_global_module(module.into()); +} +fn main() -> Result<(), Box> { + 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()); + { + ::std::io::_print(format_args!("--- Registered Functions ---\n")); + }; + for metadata_clone in engine + .collect_fn_metadata( + None, + |info: rhai::FuncInfo<'_>| Some(info.metadata.clone()), + true, + ) + { + if metadata_clone.name == "get_collection" { + { + ::std::io::_print( + format_args!( + "Found get_collection function, args: {0:?}\n", + metadata_clone.param_types, + ), + ); + }; + } + } + { + ::std::io::_print(format_args!("--------------------------\n")); + }; + 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(&db, "alice_pk", "Collection", coll1.id()); + grant_access(&db, "bob_pk", "Collection", coll2.id()); + grant_access(&db, "alice_pk", "Collection", coll2.id()); + grant_access(&db, "general_user_pk", "Collection", coll3.id()); + { + ::std::io::_print(format_args!("--- Rhai Authorization Example ---\n")); + }; + let mut scope = Scope::new(); + 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)); + 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()); + db_config.insert("CIRCLE_PUBLIC_KEY".into(), "alice_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + { + ::std::io::_print( + format_args!("Alice accessing her collection 1: Success, title\n"), + ); + }; + let result = engine.eval::>("get_collection(1)")?; + let result_clone = result.clone().expect("REASON"); + { + ::std::io::_print( + format_args!( + "Alice accessing her collection 1: Success, title = {0}\n", + result_clone.title, + ), + ); + }; + match (&result_clone.id(), &1) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + let kind = ::core::panicking::AssertKind::Eq; + ::core::panicking::assert_failed( + kind, + &*left_val, + &*right_val, + ::core::option::Option::None, + ); + } + } + }; + 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()); + db_config.insert("CIRCLE_PUBLIC_KEY".into(), "alice_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + let result = engine + .eval_with_scope::>(&mut scope, "get_collection(1)")?; + { + ::std::io::_print( + format_args!( + "Bob accessing Alice\'s collection 1: Failure as expected ({0:?})\n", + result, + ), + ); + }; + if !result.is_none() { + ::core::panicking::panic("assertion failed: result.is_none()") + } + 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()); + db_config.insert("CIRCLE_PUBLIC_KEY".into(), "bob_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + let result: Option = engine + .eval_with_scope::>(&mut scope, "get_collection(2)")?; + let collection = result.expect("Alice should have access to Bob's collection"); + { + ::std::io::_print( + format_args!( + "Alice accessing Bob\'s collection 2: Success, title = {0}\n", + collection.title, + ), + ); + }; + match (&collection.id(), &2) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + let kind = ::core::panicking::AssertKind::Eq; + ::core::panicking::assert_failed( + kind, + &*left_val, + &*right_val, + ::core::option::Option::None, + ); + } + } + }; + 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::(&mut scope, "list_all_collections()") + .unwrap(); + { + ::std::io::_print( + format_args!("General user listing collections: Found {0}\n", result.0.len()), + ); + }; + match (&result.0.len(), &1) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + let kind = ::core::panicking::AssertKind::Eq; + ::core::panicking::assert_failed( + kind, + &*left_val, + &*right_val, + ::core::option::Option::None, + ); + } + } + }; + match (&result.0[0].id(), &3) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + let kind = ::core::panicking::AssertKind::Eq; + ::core::panicking::assert_failed( + kind, + &*left_val, + &*right_val, + ::core::option::Option::None, + ); + } + } + }; + 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::(&mut scope, "list_all_collections()") + .unwrap(); + { + ::std::io::_print( + format_args!("Alice listing collections: Found {0}\n", collections.0.len()), + ); + }; + match (&collections.0.len(), &2) { + (left_val, right_val) => { + if !(*left_val == *right_val) { + let kind = ::core::panicking::AssertKind::Eq; + ::core::panicking::assert_failed( + kind, + &*left_val, + &*right_val, + ::core::option::Option::None, + ); + } + } + }; + let ids: Vec = collections.0.iter().map(|c| c.id()).collect(); + if !(ids.contains(&1) && ids.contains(&2)) { + ::core::panicking::panic( + "assertion failed: ids.contains(&1) && ids.contains(&2)", + ) + } + Ok(()) +} diff --git a/src/dsl/src/lib.rs b/src/dsl/src/lib.rs index 4161203..bb74a08 100644 --- a/src/dsl/src/lib.rs +++ b/src/dsl/src/lib.rs @@ -1,6 +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; \ No newline at end of file +pub use macros::register_authorized_get_by_id_fn; +pub use macros::register_authorized_list_fn; +pub use macros::id_from_i64_to_u32; \ No newline at end of file diff --git a/src/authorization/Cargo.toml b/src/macros/Cargo.toml similarity index 92% rename from src/authorization/Cargo.toml rename to src/macros/Cargo.toml index b1598cc..f3c4587 100644 --- a/src/authorization/Cargo.toml +++ b/src/macros/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "authorization" +name = "macros" version = "0.1.0" edition = "2024" diff --git a/src/macros/examples/access_control.rs b/src/macros/examples/access_control.rs new file mode 100644 index 0000000..8180e81 --- /dev/null +++ b/src/macros/examples/access_control.rs @@ -0,0 +1,191 @@ +use macros::{register_authorized_get_by_id_fn, register_authorized_list_fn}; +use rhai::{Engine, Module, Position, Scope, Dynamic}; +use std::sync::Arc; + +// 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, +}; + +use rhai::{FuncRegistration, EvalAltResult}; // For macro expansion + +// Rewritten to match the new `Access` model structure. +fn grant_access(db: &Arc, 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) { + let mut module = Module::new(); + + register_authorized_get_by_id_fn!( + module: &mut module, + 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 create_alice_engine(db_dir: &str, alice_pk: &str) -> Engine { + let mut engine = Engine::new(); + + let db_path = format!("{}/{}", db_dir, alice_pk); + let db = Arc::new(OurDB::new(&db_path, false).expect("Failed to create DB")); + + // 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 coll3 = Collection::new() + .title("General Collection") + .description("This is a general collection"); + + db.set(&coll1).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, "user_pk", "Collection", coll3.id()); + + register_example_module(&mut engine, db.clone()); + let mut db_config = rhai::Map::new(); + db_config.insert("DB_PATH".into(), db_dir.clone().into()); + db_config.insert("CIRCLE_PUBLIC_KEY".into(), "alice_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + engine +} + +fn create_bob_engine(db_dir: &str, bob_pk: &str) -> Engine { + let mut engine = Engine::new(); + + let db_path = format!("{}/{}", db_dir, bob_pk); + let db = Arc::new(OurDB::new(db_path, false).expect("Failed to create DB")); + + let coll2 = Collection::new() + .title("Bob's Shared Collection") + .description("This is Bob's shared collection Alice has access."); + + db.set(&coll2).expect("Failed to set collection"); + grant_access(&db, "alice_pk", "Collection", coll2.id()); + + register_example_module(&mut engine, db.clone()); + let mut db_config = rhai::Map::new(); + db_config.insert("DB_PATH".into(), db_dir.clone().into()); + db_config.insert("CIRCLE_PUBLIC_KEY".into(), "bob_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + engine +} + +fn create_user_engine(db_dir: &str, user_pk: &str) -> Engine { + let mut engine = Engine::new(); + + let db_path = format!("{}/{}", db_dir, user_pk); + let db = Arc::new(OurDB::new(db_path, false).expect("Failed to create DB")); + register_example_module(&mut engine, db.clone()); + let mut db_config = rhai::Map::new(); + db_config.insert("DB_PATH".into(), db_dir.clone().into()); + db_config.insert("CIRCLE_PUBLIC_KEY".into(), "user_pk".into()); + engine.set_default_tag(Dynamic::from(db_config)); + engine +} + +fn main() -> Result<(), Box> { + let db_path = format!("{}/hero/db", std::env::var("HOME").unwrap()); + let alice_pk = "alice_pk"; + let bob_pk = "bob_pk"; + let user_pk = "user_pk"; + + let mut engine_alice = create_alice_engine(&db_path, alice_pk); + let mut engine_bob = create_bob_engine(&db_path, bob_pk); + let mut engine_user = create_user_engine(&db_path, user_pk); + + println!("--------------------------"); + println!("--- Rhai Authorization Example ---"); + + let mut scope = Scope::new(); + + // Create a Dynamic value holding your DB path or a config object + { + let mut tag_dynamic = engine_alice.default_tag_mut().as_map_mut().unwrap(); + tag_dynamic.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into()); + } + // engine_alice.set_default_tag(Dynamic::from(tag_dynamic.clone())); + + println!("Alice accessing her collection 1: Success, title"); // Access field directly + let result = engine_alice.eval::>("get_collection(1)")?; + let result_clone = result.clone().expect("Failed to retrieve collection. It might not exist or you may not have access."); + 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 tag_dynamic = engine_bob.default_tag_mut().as_map_mut().unwrap(); + tag_dynamic.insert("CALLER_PUBLIC_KEY".into(), "bob_pk".into()); + } + let result = engine_alice.eval_with_scope::>(&mut scope, "get_collection(1)")?; + println!("Bob accessing Alice's collection 1: Failure as expected ({:?})", result); + assert!(result.is_none()); + + // Scenario 3: Alice accesses Bob's collection (Success) + let mut db_config = rhai::Map::new(); + db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into()); + engine_bob.set_default_tag(Dynamic::from(db_config)); + let result: Option = engine_bob.eval_with_scope::>(&mut scope, "get_collection(2)")?; + let collection = result.expect("Alice should have access to Bob's collection"); + println!("Alice accessing Bob's collection 2: Success, title = {}", collection.title); // Access field directly + assert_eq!(collection.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_user.set_default_tag(Dynamic::from(db_config)); + let result = engine_user.eval_with_scope::(&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_alice.set_default_tag(Dynamic::from(db_config)); + let collections = engine_alice.eval_with_scope::(&mut scope, "list_all_collections()").unwrap(); + println!("Alice listing collections: Found {}", collections.0.len()); + assert_eq!(collections.0.len(), 2); + let ids: Vec = collections.0.iter().map(|c| c.id()).collect(); + assert!(ids.contains(&1) && ids.contains(&2)); + + Ok(()) +} diff --git a/src/authorization/src/lib.rs b/src/macros/src/lib.rs similarity index 80% rename from src/authorization/src/lib.rs rename to src/macros/src/lib.rs index ad79f19..cb2f6af 100644 --- a/src/authorization/src/lib.rs +++ b/src/macros/src/lib.rs @@ -14,9 +14,7 @@ //! 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 rhai::{EvalAltResult, Position, FuncRegistration}; use std::convert::TryFrom; /// Extracts the `CALLER_PUBLIC_KEY` string constant from the Rhai `NativeCallContext`. @@ -78,14 +76,10 @@ pub fn id_from_i64_to_u32(id_i64: i64) -> Result> { macro_rules! register_authorized_get_by_id_fn { ( module: $module:expr, - db_clone: $db_clone:expr, // Cloned Arc 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, Box> { @@ -100,26 +94,59 @@ macro_rules! register_authorized_get_by_id_fn { 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 db_path = tag_map.get("DB_PATH") + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'DB_PATH' not found in context tag Map.".into(), context.position())))?; + + let db_path = db_path.clone().into_string()?; + + println!("DB Path: {}", db_path); + + let circle_pk = tag_map.get("CIRCLE_PUBLIC_KEY") + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CIRCLE_PUBLIC_KEY' not found in context tag Map.".into(), context.position())))?; + + let circle_pk = circle_pk.clone().into_string()?; + + let db_path = format!("{}/{}", db_path, circle_pk); + let db = Arc::new(OurDB::new(db_path, false).expect("Failed to create DB")); + 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, - ); + println!("Checking access for public key: {}", caller_pk_str); + if circle_pk != caller_pk_str { + // Use the standalone can_access_resource function from heromodels + let has_access = heromodels::models::access::access::can_access_resource( + db.clone(), + &caller_pk_str, + actual_id, + $resource_type_str, + ); if !has_access { return Ok(None); } + } + + let all_items: Vec<$rhai_return_rust_type> = db + .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 result = db_instance_fetch.get_by_id(actual_id).map_err(|e| { + for item in all_items { + println!("{} with ID: {}", $resource_type_str, item.id()); + } + println!("Fetching {} with ID: {}", $resource_type_str, actual_id); + + + + let result = db.get_by_id(actual_id).map_err(|e| { + println!("Database error fetching {} with ID: {}", $resource_type_str, actual_id); Box::new(EvalAltResult::ErrorRuntime( format!("Database error fetching {}: {:?}", $resource_type_str, e).into(), context.position(), )) })?; + println!("Database fetched"); Ok(result) }, ); @@ -194,4 +221,4 @@ macro_rules! register_authorized_list_fn { }, ); }; -} +} \ No newline at end of file diff --git a/src/worker/src/lib.rs b/src/worker/src/lib.rs index 68457e7..9d1effb 100644 --- a/src/worker/src/lib.rs +++ b/src/worker/src/lib.rs @@ -1,7 +1,7 @@ use chrono::Utc; use log::{debug, error, info}; use redis::AsyncCommands; -use rhai::{Engine, Scope}; +use rhai::{Dynamic, Engine, Scope}; use rhai_client::RhaiTaskDetails; // Import for constructing the reply message use serde_json; use std::collections::HashMap; @@ -44,7 +44,8 @@ async fn update_task_status_in_redis( pub fn spawn_rhai_worker( _circle_id: u32, // For logging or specific logic if needed in the future circle_public_key: String, - engine: Engine, + db_path: String, + mut engine: Engine, redis_url: String, mut shutdown_rx: mpsc::Receiver<()>, // Add shutdown receiver preserve_tasks: bool, // Flag to control task cleanup @@ -86,12 +87,12 @@ pub fn spawn_rhai_worker( tokio::select! { // Listen for shutdown signal _ = shutdown_rx.recv() => { - info!("Worker for Circle Public Key '{}': Shutdown signal received. Terminating loop.", circle_public_key); + info!("Worker for Circle Public Key '{}': Shutdown signal received. Terminating loop.", circle_public_key.clone()); break; } // Listen for tasks from Redis blpop_result = redis_conn.blpop(&blpop_keys, BLPOP_TIMEOUT_SECONDS as f64) => { - debug!("Worker for Circle Public Key '{}': Attempting BLPOP on queue: {}", circle_public_key, queue_key); + debug!("Worker for Circle Public Key '{}': Attempting BLPOP on queue: {}", circle_public_key.clone(), queue_key); let response: Option<(String, String)> = match blpop_result { Ok(resp) => resp, Err(e) => { @@ -127,23 +128,19 @@ pub fn spawn_rhai_worker( debug!("Worker for Circle Public Key '{}', Task {}: Status updated to 'processing'.", circle_public_key, task_id); } - let mut scope = Scope::new(); - scope.push_constant("CIRCLE_PUBLIC_KEY", circle_public_key.clone()); - debug!("Worker for Circle Public Key '{}', Task {}: Injected CIRCLE_PUBLIC_KEY into scope.", circle_public_key, task_id); - - if let Some(public_key) = public_key_opt.as_deref() { - if !public_key.is_empty() { - scope.push_constant("CALLER_PUBLIC_KEY", public_key.to_string()); - debug!("Worker for Circle Public Key '{}', Task {}: Injected CALLER_PUBLIC_KEY into scope.", circle_public_key, task_id); - } - } + let mut db_config = rhai::Map::new(); + db_config.insert("DB_PATH".into(), db_path.clone().into()); + db_config.insert("CALLER_PUBLIC_KEY".into(), public_key_opt.unwrap_or_default().into()); + db_config.insert("CIRCLE_PUBLIC_KEY".into(), circle_public_key.clone().into()); + engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions + debug!("Worker for Circle Public Key '{}', Task {}: Evaluating script with Rhai engine.", circle_public_key, task_id); let mut final_status = "error".to_string(); // Default to error let mut final_output: Option = None; let mut final_error_msg: Option = None; - match engine.eval_with_scope::(&mut scope, &script_content) { + match engine.eval::(&script_content) { Ok(result) => { let output_str = if result.is::() { // If the result is a string, we can unwrap it directly. @@ -194,7 +191,7 @@ pub fn spawn_rhai_worker( created_at, // Original creation time updated_at: Utc::now(), // Time of this final update/reply // reply_to_queue is no longer a field - public_key: public_key_opt, + public_key: public_key_opt.clone(), }; match serde_json::to_string(&reply_details) { Ok(reply_json) => {