implement auth macros

This commit is contained in:
Timur Gordon 2025-06-23 22:15:06 +02:00
parent 8c88695953
commit aa4712b8af
10 changed files with 740 additions and 103 deletions

22
Cargo.lock generated
View File

@ -140,16 +140,6 @@ 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"
@ -1653,6 +1643,16 @@ version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "macros"
version = "0.1.0"
dependencies = [
"heromodels",
"heromodels_core",
"rhai",
"serde",
]
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@ -2378,11 +2378,11 @@ dependencies = [
name = "rhailib_dsl" name = "rhailib_dsl"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"authorization",
"chrono", "chrono",
"heromodels", "heromodels",
"heromodels-derive", "heromodels-derive",
"heromodels_core", "heromodels_core",
"macros",
"rhai", "rhai",
"serde", "serde",
"serde_json", "serde_json",

View File

@ -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/authorization", "src/dsl", "src/rhai_engine_ui", "src/macros", "src/dsl",
] ]
resolver = "2" # Recommended for new workspaces resolver = "2" # Recommended for new workspaces

View File

@ -10,7 +10,7 @@ heromodels = { path = "../../../db/heromodels", features = ["rhai"] }
heromodels_core = { path = "../../../db/heromodels_core" } heromodels_core = { path = "../../../db/heromodels_core" }
chrono = "0.4" chrono = "0.4"
heromodels-derive = { path = "../../../db/heromodels-derive" } heromodels-derive = { path = "../../../db/heromodels-derive" }
authorization = { path = "../authorization"} macros = { path = "../macros"}
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View File

@ -34,7 +34,6 @@ fn register_example_module(engine: &mut Engine, db: Arc<OurDB>) {
register_authorized_get_by_id_fn!( register_authorized_get_by_id_fn!(
module: &mut module, module: &mut module,
db_clone: db.clone(),
rhai_fn_name: "get_collection", rhai_fn_name: "get_collection",
resource_type_str: "Collection", resource_type_str: "Collection",
rhai_return_rust_type: heromodels::models::library::collection::Collection // Use Collection struct 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<OurDB>) {
engine.register_global_module(module.into()); engine.register_global_module(module.into());
} }
fn main() -> Result<(), Box<rhai::EvalAltResult>> { fn create_alice_engine(db_dir: &str, alice_pk: &str) -> Engine {
let mut engine = Engine::new(); let mut engine = Engine::new();
let temp_dir = tempdir().unwrap(); let db_path = format!("{}/{}", db_dir, alice_pk);
let db = Arc::new(OurDB::new(temp_dir.path(), false).expect("Failed to create DB")); let db = Arc::new(OurDB::new(&db_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. // Populate DB using the new `create_collection` helper.
// Ownership is no longer on the collection itself, so we don't need owner_pk here. // Ownership is no longer on the collection itself, so we don't need owner_pk here.
let coll = Collection::new() let coll = Collection::new()
@ -83,70 +69,111 @@ fn main() -> Result<(), Box<rhai::EvalAltResult>> {
let coll1 = Collection::new() let coll1 = Collection::new()
.title("Alice's Private Collection") .title("Alice's Private Collection")
.description("This is 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() let coll3 = Collection::new()
.title("General Collection") .title("General Collection")
.description("This is a general collection"); .description("This is a general collection");
db.set(&coll1).expect("Failed to set 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"); db.set(&coll3).expect("Failed to set collection");
// Grant access based on the new model. // Grant access based on the new model.
grant_access(&db, "alice_pk", "Collection", coll1.id()); grant_access(&db, "alice_pk", "Collection", coll1.id());
grant_access(&db, "bob_pk", "Collection", coll2.id()); grant_access(&db, "user_pk", "Collection", coll3.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()); 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<rhai::EvalAltResult>> {
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 ---"); println!("--- Rhai Authorization Example ---");
let mut scope = Scope::new(); 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 // 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()); let mut tag_dynamic = engine_alice.default_tag_mut().as_map_mut().unwrap();
db_config.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into()); tag_dynamic.insert("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
engine.set_default_tag(Dynamic::from(db_config)); }
// engine_alice.set_default_tag(Dynamic::from(tag_dynamic.clone()));
println!("Alice accessing her collection 1: Success, title"); // Access field directly println!("Alice accessing her collection 1: Success, title"); // Access field directly
let result = engine.eval::<Option<Collection>>("get_collection(1)")?; let result = engine_alice.eval::<Option<Collection>>("get_collection(1)")?;
let result_clone = result.clone().expect("REASON"); 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 println!("Alice accessing her collection 1: Success, title = {}", result_clone.title); // Access field directly
assert_eq!(result_clone.id(), 1); assert_eq!(result_clone.id(), 1);
// Scenario 2: Bob tries to access Alice's collection (Failure) // 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()); let mut tag_dynamic = engine_bob.default_tag_mut().as_map_mut().unwrap();
db_config.insert("CALLER_PUBLIC_KEY".into(), "bob_pk".into()); tag_dynamic.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)"); let result = engine_alice.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(1)")?;
println!("Bob accessing Alice's collection 1: {:?}", result); println!("Bob accessing Alice's collection 1: Failure as expected ({:?})", result);
let result_clone = result.expect("REASON"); assert!(result.is_none());
println!("Bob accessing Alice's collection 1: {:?}", result_clone);
// assert!(result_clone.is_none());
// Scenario 3: Alice accesses Bob's collection (Success) // Scenario 3: Alice accesses Bob's collection (Success)
let mut db_config = rhai::Map::new(); 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("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
engine.set_default_tag(Dynamic::from(db_config)); engine_bob.set_default_tag(Dynamic::from(db_config));
let result = engine.eval_with_scope::<Collection>(&mut scope, "get_collection(2)")?; let result: Option<Collection> = engine_bob.eval_with_scope::<Option<Collection>>(&mut scope, "get_collection(2)")?;
println!("Alice accessing Bob's collection 2: Success, title = {}", result.title); // Access field directly let collection = result.expect("Alice should have access to Bob's collection");
assert_eq!(result.id(), 2); 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) // Scenario 4: General user lists collections (Sees 1)
let mut db_config = rhai::Map::new(); let mut db_config = rhai::Map::new();
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into()); db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into());
db_config.insert("CALLER_PUBLIC_KEY".into(), "general_user_pk".into()); db_config.insert("CALLER_PUBLIC_KEY".into(), "general_user_pk".into());
engine.set_default_tag(Dynamic::from(db_config)); engine_user.set_default_tag(Dynamic::from(db_config));
let result = engine.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()").unwrap(); let result = engine_user.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()").unwrap();
println!("General user listing collections: Found {}", result.0.len()); println!("General user listing collections: Found {}", result.0.len());
assert_eq!(result.0.len(), 1); assert_eq!(result.0.len(), 1);
assert_eq!(result.0[0].id(), 3); assert_eq!(result.0[0].id(), 3);
@ -155,8 +182,8 @@ fn main() -> Result<(), Box<rhai::EvalAltResult>> {
let mut db_config = rhai::Map::new(); let mut db_config = rhai::Map::new();
db_config.insert("db_path".into(), "actual/path/to/db.sqlite".into()); 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("CALLER_PUBLIC_KEY".into(), "alice_pk".into());
engine.set_default_tag(Dynamic::from(db_config)); engine_alice.set_default_tag(Dynamic::from(db_config));
let collections = engine.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()").unwrap(); let collections = engine_alice.eval_with_scope::<RhaiCollectionArray>(&mut scope, "list_all_collections()").unwrap();
println!("Alice listing collections: Found {}", collections.0.len()); println!("Alice listing collections: Found {}", collections.0.len());
assert_eq!(collections.0.len(), 2); assert_eq!(collections.0.len(), 2);
let ids: Vec<u32> = collections.0.iter().map(|c| c.id()).collect(); let ids: Vec<u32> = collections.0.iter().map(|c| c.id()).collect();

View File

@ -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<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");
}
fn register_example_module(engine: &mut Engine, db: Arc<OurDB>) {
let mut module = Module::new();
FuncRegistration::new("get_collection")
.set_into_module(
&mut module,
move |
context: rhai::NativeCallContext,
id_val: i64,
| -> Result<
Option<heromodels::models::library::collection::Collection>,
Box<EvalAltResult>,
> {
let actual_id: u32 = ::macros::id_from_i64_to_u32(id_val)?;
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 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<EvalAltResult>,
> {
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<
heromodels::models::library::collection::Collection,
> = db_instance_fetch
.collection::<heromodels::models::library::collection::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<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());
{
::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::<Option<Collection>>("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::<Option<Collection>>(&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<Collection> = engine
.eval_with_scope::<Option<Collection>>(&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::<RhaiCollectionArray>(&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::<RhaiCollectionArray>(&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<u32> = 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(())
}

View File

@ -1,6 +1,6 @@
pub mod library; pub mod library;
pub mod access; pub mod access;
pub use authorization::register_authorized_get_by_id_fn; pub use macros::register_authorized_get_by_id_fn;
pub use authorization::register_authorized_list_fn; pub use macros::register_authorized_list_fn;
pub use authorization::id_from_i64_to_u32; pub use macros::id_from_i64_to_u32;

View File

@ -1,5 +1,5 @@
[package] [package]
name = "authorization" name = "macros"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"

View File

@ -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<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,
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<rhai::EvalAltResult>> {
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::<Option<Collection>>("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::<Option<Collection>>(&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<Collection> = engine_bob.eval_with_scope::<Option<Collection>>(&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::<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_alice.set_default_tag(Dynamic::from(db_config));
let collections = engine_alice.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(())
}

View File

@ -14,9 +14,7 @@
//! 2. The macros internally use `can_access_resource` for authorization checks. //! 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. //! 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 rhai::{EvalAltResult, Position, FuncRegistration};
use heromodels_core::Model;
use rhai::{EvalAltResult, NativeCallContext, Position};
use std::convert::TryFrom; use std::convert::TryFrom;
/// Extracts the `CALLER_PUBLIC_KEY` string constant from the Rhai `NativeCallContext`. /// 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<u32, Box<EvalAltResult>> {
macro_rules! register_authorized_get_by_id_fn { macro_rules! register_authorized_get_by_id_fn {
( (
module: $module:expr, 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") 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") 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`) 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( FuncRegistration::new($rhai_fn_name).set_into_module(
$module, $module,
move |context: rhai::NativeCallContext, id_val: i64| -> Result<Option<$rhai_return_rust_type>, Box<EvalAltResult>> { move |context: rhai::NativeCallContext, id_val: i64| -> Result<Option<$rhai_return_rust_type>, Box<EvalAltResult>> {
@ -100,26 +94,59 @@ macro_rules! register_authorized_get_by_id_fn {
let pk_dynamic = tag_map.get("CALLER_PUBLIC_KEY") 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())))?; .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()?; let caller_pk_str = pk_dynamic.clone().into_string()?;
// Use the standalone can_access_resource function from heromodels println!("Checking access for public key: {}", caller_pk_str);
let has_access = heromodels::models::access::access::can_access_resource( if circle_pk != caller_pk_str {
db_instance_auth.clone(), // Use the standalone can_access_resource function from heromodels
&caller_pk_str, let has_access = heromodels::models::access::access::can_access_resource(
actual_id, db.clone(),
$resource_type_str, &caller_pk_str,
); actual_id,
$resource_type_str,
);
if !has_access { if !has_access {
return Ok(None); 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( Box::new(EvalAltResult::ErrorRuntime(
format!("Database error fetching {}: {:?}", $resource_type_str, e).into(), format!("Database error fetching {}: {:?}", $resource_type_str, e).into(),
context.position(), context.position(),
)) ))
})?; })?;
println!("Database fetched");
Ok(result) Ok(result)
}, },
); );
@ -194,4 +221,4 @@ macro_rules! register_authorized_list_fn {
}, },
); );
}; };
} }

View File

@ -1,7 +1,7 @@
use chrono::Utc; use chrono::Utc;
use log::{debug, error, info}; use log::{debug, error, info};
use redis::AsyncCommands; use redis::AsyncCommands;
use rhai::{Engine, Scope}; use rhai::{Dynamic, Engine, Scope};
use rhai_client::RhaiTaskDetails; // Import for constructing the reply message use rhai_client::RhaiTaskDetails; // Import for constructing the reply message
use serde_json; use serde_json;
use std::collections::HashMap; use std::collections::HashMap;
@ -44,7 +44,8 @@ async fn update_task_status_in_redis(
pub fn spawn_rhai_worker( pub fn spawn_rhai_worker(
_circle_id: u32, // For logging or specific logic if needed in the future _circle_id: u32, // For logging or specific logic if needed in the future
circle_public_key: String, circle_public_key: String,
engine: Engine, db_path: String,
mut engine: Engine,
redis_url: String, redis_url: String,
mut shutdown_rx: mpsc::Receiver<()>, // Add shutdown receiver mut shutdown_rx: mpsc::Receiver<()>, // Add shutdown receiver
preserve_tasks: bool, // Flag to control task cleanup preserve_tasks: bool, // Flag to control task cleanup
@ -86,12 +87,12 @@ pub fn spawn_rhai_worker(
tokio::select! { tokio::select! {
// Listen for shutdown signal // Listen for shutdown signal
_ = shutdown_rx.recv() => { _ = 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; break;
} }
// Listen for tasks from Redis // Listen for tasks from Redis
blpop_result = redis_conn.blpop(&blpop_keys, BLPOP_TIMEOUT_SECONDS as f64) => { 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 { let response: Option<(String, String)> = match blpop_result {
Ok(resp) => resp, Ok(resp) => resp,
Err(e) => { 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); debug!("Worker for Circle Public Key '{}', Task {}: Status updated to 'processing'.", circle_public_key, task_id);
} }
let mut scope = Scope::new(); let mut db_config = rhai::Map::new();
scope.push_constant("CIRCLE_PUBLIC_KEY", circle_public_key.clone()); db_config.insert("DB_PATH".into(), db_path.clone().into());
debug!("Worker for Circle Public Key '{}', Task {}: Injected CIRCLE_PUBLIC_KEY into scope.", circle_public_key, task_id); 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());
if let Some(public_key) = public_key_opt.as_deref() { engine.set_default_tag(Dynamic::from(db_config)); // Or pass via CallFnOptions
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);
}
}
debug!("Worker for Circle Public Key '{}', Task {}: Evaluating script with Rhai engine.", circle_public_key, task_id); 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_status = "error".to_string(); // Default to error
let mut final_output: Option<String> = None; let mut final_output: Option<String> = None;
let mut final_error_msg: Option<String> = None; let mut final_error_msg: Option<String> = None;
match engine.eval_with_scope::<rhai::Dynamic>(&mut scope, &script_content) { match engine.eval::<rhai::Dynamic>(&script_content) {
Ok(result) => { Ok(result) => {
let output_str = if result.is::<String>() { let output_str = if result.is::<String>() {
// If the result is a string, we can unwrap it directly. // 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 created_at, // Original creation time
updated_at: Utc::now(), // Time of this final update/reply updated_at: Utc::now(), // Time of this final update/reply
// reply_to_queue is no longer a field // 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) { match serde_json::to_string(&reply_details) {
Ok(reply_json) => { Ok(reply_json) => {