From 5d8189a65323e5373eb9860bca0a8ba0066f41df Mon Sep 17 00:00:00 2001 From: Timur Gordon <31495328+timurgordon@users.noreply.github.com> Date: Thu, 20 Nov 2025 08:51:52 +0100 Subject: [PATCH] feat(context): add Context model with admin-based ACL system --- Cargo.toml | 1 + bin/runners/osiris/examples/engine.rs | 197 +++++++++++++++ lib/models/context/Cargo.toml | 18 ++ lib/models/context/src/access.rs | 181 ++++++++++++++ lib/models/context/src/lib.rs | 343 ++++++++++++++++++++++++++ lib/models/context/src/rhai.rs | 327 ++++++++++++++++++++++++ lib/models/context/src/rhai_old.rs | 333 +++++++++++++++++++++++++ 7 files changed, 1400 insertions(+) create mode 100644 bin/runners/osiris/examples/engine.rs create mode 100644 lib/models/context/Cargo.toml create mode 100644 lib/models/context/src/access.rs create mode 100644 lib/models/context/src/lib.rs create mode 100644 lib/models/context/src/rhai.rs create mode 100644 lib/models/context/src/rhai_old.rs diff --git a/Cargo.toml b/Cargo.toml index 9ec184f..ad8d962 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "lib/clients/osiris", "lib/clients/supervisor", "lib/models/job", + "lib/models/context", "lib/osiris/core", "lib/osiris/derive", "lib/runner", diff --git a/bin/runners/osiris/examples/engine.rs b/bin/runners/osiris/examples/engine.rs new file mode 100644 index 0000000..144840c --- /dev/null +++ b/bin/runners/osiris/examples/engine.rs @@ -0,0 +1,197 @@ +//! Osiris Engine Example +//! +//! This example demonstrates how to: +//! 1. Create an Osiris Rhai engine with all registered functions +//! 2. Execute Rhai scripts using the actual Osiris API +//! 3. Test context creation, save, get, list, delete operations +//! +//! Run with: cargo run --example engine -p runner-osiris + +use rhai::{Dynamic, Map}; + +// Import the actual engine creation function +mod engine_impl { + include!("../src/engine.rs"); +} + +use engine_impl::create_osiris_engine; + +#[tokio::main] +async fn main() -> Result<(), Box> { + println!("šŸš€ Osiris Engine Example\n"); + println!("==========================================\n"); + + // Create the engine with all Osiris functions registered + let mut engine = create_osiris_engine()?; + + // Set up context tags (simulating what the runner does) + let mut tag_map = Map::new(); + let signatories: rhai::Array = vec![ + Dynamic::from("pk1".to_string()), + Dynamic::from("pk2".to_string()), + ]; + tag_map.insert("SIGNATORIES".into(), Dynamic::from(signatories)); + tag_map.insert("CALLER_ID".into(), "test-caller".to_string().into()); + tag_map.insert("CONTEXT_ID".into(), "test-context".to_string().into()); + engine.set_default_tag(Dynamic::from(tag_map)); + + // Test 1: Simple Rhai script + println!("šŸ“ Test 1: Simple Rhai Script"); + let script = r#" + let x = 10; + let y = 20; + x + y + "#; + + match engine.eval::(script) { + Ok(result) => println!(" āœ“ Result: {}\n", result), + Err(e) => println!(" āœ— Error: {}\n", e), + } + + // Test 2: Get context (Osiris function) + println!("šŸ“ Test 2: Get Context"); + let context_script = r#" + // Get context with participants (must be signatories) + let ctx = get_context(["pk1", "pk2"]); + ctx.context_id() + "#; + + match engine.eval::(context_script) { + Ok(result) => println!(" āœ“ Context ID: {}\n", result), + Err(e) => println!(" āœ— Error: {}\n", e), + } + + // Test 3: Create a Note and save it + println!("šŸ“ Test 3: Create and Save a Note"); + let note_script = r#" + let ctx = get_context(["pk1"]); + // Use the builder-style API + let my_note = note("test-note-123") + .title("Test Note") + .content("This is a test note"); + ctx.save(my_note); + "Note saved successfully" + "#; + + match engine.eval::(note_script) { + Ok(result) => println!(" āœ“ {}\n", result), + Err(e) => println!(" āœ— Error: {}\n", e), + } + + // Test 4: Get from collection + println!("šŸ“ Test 4: Get from Collection"); + let get_script = r#" + let ctx = get_context(["pk1"]); + // Try to get a note (will fail if doesn't exist, but shows the API works) + ctx.get("notes", "test-note-123") + "#; + + match engine.eval::(get_script) { + Ok(result) => println!(" āœ“ Result: {:?}\n", result), + Err(e) => println!(" ⚠ Error (expected if note doesn't exist): {}\n", e), + } + + // Test 5: List from collection + println!("šŸ“ Test 5: List from Collection"); + let list_script = r#" + let ctx = get_context(["pk1"]); + // List all notes in the context + ctx.list("notes") + "#; + + match engine.eval::(list_script) { + Ok(result) => println!(" āœ“ Result: {:?}\n", result), + Err(e) => println!(" ⚠ Error: {}\n", e), + } + + // Test 6: Delete from collection + println!("šŸ“ Test 6: Delete from Collection"); + let delete_script = r#" + let ctx = get_context(["pk1"]); + // Try to delete a note + ctx.delete("notes", "test-note-123") + "#; + + match engine.eval::(delete_script) { + Ok(result) => println!(" āœ“ Result: {:?}\n", result), + Err(e) => println!(" ⚠ Error (expected if note doesn't exist): {}\n", e), + } + + // Test 7: Create an Event + println!("šŸ“ Test 7: Create and Save an Event"); + let event_script = r#" + let ctx = get_context(["pk1"]); + // event() takes (namespace, title) in the module version + let my_event = event("test-event-123", "Test Event") + .description("This is a test event"); + ctx.save(my_event); + "Event saved successfully" + "#; + + match engine.eval::(event_script) { + Ok(result) => println!(" āœ“ {}\n", result), + Err(e) => println!(" āœ— Error: {}\n", e), + } + + // Test 8: Create a User (HeroLedger) + println!("šŸ“ Test 8: Create and Save a User"); + let user_script = r#" + let ctx = get_context(["pk1"]); + let my_user = new_user() + .username("testuser") + .add_email("test@example.com") + .pubkey("pk1"); + ctx.save(my_user); + "User saved successfully" + "#; + + match engine.eval::(user_script) { + Ok(result) => println!(" āœ“ {}\n", result), + Err(e) => println!(" āœ— Error: {}\n", e), + } + + // Test 9: Create a Group (HeroLedger) + println!("šŸ“ Test 9: Create and Save a Group"); + let group_script = r#" + let ctx = get_context(["pk1"]); + let my_group = new_group() + .name("Test Group") + .description("A test group"); + ctx.save(my_group); + "Group saved successfully" + "#; + + match engine.eval::(group_script) { + Ok(result) => println!(" āœ“ {}\n", result), + Err(e) => println!(" āœ— Error: {}\n", e), + } + + // Test 10: List users + println!("šŸ“ Test 10: List Users from Collection"); + let list_users_script = r#" + let ctx = get_context(["pk1"]); + ctx.list("users") + "#; + + match engine.eval::(list_users_script) { + Ok(result) => println!(" āœ“ Users: {:?}\n", result), + Err(e) => println!(" ⚠ Error: {}\n", e), + } + + println!("=========================================="); + println!("šŸŽ‰ All tests completed!\n"); + println!("šŸ“š Available Object Types:"); + println!(" - Note: note(id).title(...).content(...)"); + println!(" - Event: event(id, title).description(...)"); + println!(" - User: new_user().username(...).add_email(...).pubkey(...)"); + println!(" - Group: new_group().name(...).description(...)"); + println!(" - Account: new_account()..."); + println!(" - And many more: KycSession, FlowTemplate, FlowInstance, Contract, etc."); + println!("\nšŸ“– Available Operations:"); + println!(" - ctx.save(object) - Save an object"); + println!(" - ctx.get(collection, id) - Get an object by ID"); + println!(" - ctx.list(collection) - List all objects in collection"); + println!(" - ctx.delete(collection, id) - Delete an object"); + + Ok(()) +} diff --git a/lib/models/context/Cargo.toml b/lib/models/context/Cargo.toml new file mode 100644 index 0000000..38f2a2e --- /dev/null +++ b/lib/models/context/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "hero-context" +version.workspace = true +edition.workspace = true +description = "Context model for Hero platform" +license = "MIT OR Apache-2.0" + +[dependencies] +serde.workspace = true +serde_json.workspace = true +chrono.workspace = true +rhai = { version = "1.19", features = ["sync"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +uuid.workspace = true + +[target.'cfg(target_arch = "wasm32")'.dependencies] +uuid = { workspace = true, features = ["js"] } diff --git a/lib/models/context/src/access.rs b/lib/models/context/src/access.rs new file mode 100644 index 0000000..19d54f5 --- /dev/null +++ b/lib/models/context/src/access.rs @@ -0,0 +1,181 @@ +//! Access Control Logic for Context + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Unified ACL configuration for objects +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct ObjectAcl { + /// Per-user permissions for this object type + /// Maps public key -> list of permissions + pub permissions: HashMap>, + + /// Multi-signature requirements (optional) + pub multi_sig: Option, +} + +/// Permissions for object operations +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "lowercase")] +pub enum ObjectPermission { + /// Can create new objects of this type + Create, + + /// Can read objects of this type + Read, + + /// Can update existing objects of this type + Update, + + /// Can delete objects of this type + Delete, + + /// Can list all objects of this type + List, +} + +/// SAL access control - binary permission (can call or not) +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct SalAcl { + /// List of public keys allowed to call this SAL + pub allowed_callers: Vec, + + /// Multi-signature requirements (optional) + pub multi_sig: Option, +} + +/// Global permissions - simple RWX model +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +#[serde(rename_all = "lowercase")] +pub enum GlobalPermission { + /// Can read data + Read, + + /// Can write/modify data + Write, + + /// Can execute operations + Execute, +} + +/// Multi-signature requirements +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(tag = "type", rename_all = "lowercase")] +pub enum MultiSigRequirement { + /// Require ALL specified signers to sign unanimously + Unanimous { + /// List of public keys that must ALL sign + required_signers: Vec, + }, + + /// Require a minimum number of signatures from a set + Threshold { + /// Minimum number of signatures required + min_signatures: usize, + + /// Optional: specific set of allowed signers + /// If None, any signers from the context are allowed + allowed_signers: Option>, + }, + + /// Require a percentage of signers from a set + Percentage { + /// Percentage required (0.0 to 1.0, e.g., 0.66 for 66%) + percentage: f64, + + /// Optional: specific set of allowed signers + /// If None, any signers from the context are allowed + allowed_signers: Option>, + }, +} + +impl MultiSigRequirement { + /// Check if signatories satisfy this multi-sig requirement + pub fn check(&self, signatories: &[String], total_members: usize) -> bool { + match self { + MultiSigRequirement::Unanimous { required_signers } => { + // ALL required signers must be present + required_signers.iter().all(|signer| signatories.contains(signer)) + } + MultiSigRequirement::Threshold { min_signatures, allowed_signers } => { + // Check if we have enough signatures + if signatories.len() < *min_signatures { + return false; + } + + // If allowed_signers is specified, check all signatories are in the list + if let Some(allowed) = allowed_signers { + signatories.iter().all(|sig| allowed.contains(sig)) + } else { + true + } + } + MultiSigRequirement::Percentage { percentage, allowed_signers } => { + if let Some(allowed) = allowed_signers { + // Filter signatories to only those in allowed list + let valid_sigs: Vec<_> = signatories + .iter() + .filter(|sig| allowed.contains(sig)) + .collect(); + + let required_count = (allowed.len() as f64 * percentage).ceil() as usize; + valid_sigs.len() >= required_count + } else { + // Use all context members + let required_count = (total_members as f64 * percentage).ceil() as usize; + signatories.len() >= required_count + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_multi_sig_unanimous() { + let multi_sig = MultiSigRequirement::Unanimous { + required_signers: vec!["alice".to_string(), "bob".to_string()], + }; + + // Both signers present - should pass + assert!(multi_sig.check(&["alice".to_string(), "bob".to_string()], 3)); + + // Only one signer - should fail + assert!(!multi_sig.check(&["alice".to_string()], 3)); + } + + #[test] + fn test_multi_sig_threshold() { + let multi_sig = MultiSigRequirement::Threshold { + min_signatures: 2, + allowed_signers: Some(vec!["alice".to_string(), "bob".to_string(), "charlie".to_string()]), + }; + + // 2 signatures - should pass + assert!(multi_sig.check(&["alice".to_string(), "bob".to_string()], 3)); + + // 1 signature - should fail + assert!(!multi_sig.check(&["alice".to_string()], 3)); + } + + #[test] + fn test_multi_sig_percentage() { + let multi_sig = MultiSigRequirement::Percentage { + percentage: 0.66, // 66% + allowed_signers: Some(vec![ + "alice".to_string(), + "bob".to_string(), + "charlie".to_string(), + ]), + }; + + // 2 out of 3 (66%) - should pass + assert!(multi_sig.check(&["alice".to_string(), "bob".to_string()], 3)); + + // 1 out of 3 (33%) - should fail + assert!(!multi_sig.check(&["alice".to_string()], 3)); + } +} diff --git a/lib/models/context/src/lib.rs b/lib/models/context/src/lib.rs new file mode 100644 index 0000000..d2d3741 --- /dev/null +++ b/lib/models/context/src/lib.rs @@ -0,0 +1,343 @@ +//! Context Model +//! +//! A Context represents an isolated instance/workspace where users can: +//! - Store and retrieve objects (via Osiris) +//! - Execute SALs (System Abstraction Layer functions) +//! - Collaborate with specific permissions +//! +//! The Context is the authorization boundary - all operations go through it +//! and are subject to ACL checks. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +pub mod access; +pub mod rhai; + +pub use access::*; + +/// A Context represents an isolated workspace with ACL-controlled access +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct Context { + /// Human-readable name + pub name: String, + + /// Description of this context's purpose + pub description: Option, + + /// List of admin public keys - only admins can modify ACLs + pub admins: Vec, + + /// Global permissions (RWX) - what can users do in this context? + /// Maps public key -> list of global permissions + pub global_permissions: HashMap>, + + /// Per-object-type ACLs - fine-grained control over data operations + /// Maps object type (e.g., "notes", "events") -> ACL configuration + pub object_acls: HashMap, + + /// SAL ACLs - binary permission (can call or not) + /// Maps SAL name (e.g., "launch_vm", "send_email") -> ACL configuration + pub sal_acls: HashMap, +} + +impl Default for Context { + fn default() -> Self { + Self { + name: String::new(), + description: None, + admins: Vec::new(), + global_permissions: HashMap::new(), + object_acls: HashMap::new(), + sal_acls: HashMap::new(), + } + } +} + +impl Context { + /// Create a new context with a name and initial admin + pub fn new(name: String, admin: String) -> Self { + Self { + name, + description: None, + admins: vec![admin], + global_permissions: HashMap::new(), + object_acls: HashMap::new(), + sal_acls: HashMap::new(), + } + } + + /// Check if a user is an admin + pub fn is_admin(&self, pubkey: &str) -> bool { + self.admins.contains(&pubkey.to_string()) + } + + /// Check if a user has a global permission + pub fn has_global_permission(&self, pubkey: &str, permission: &GlobalPermission) -> bool { + self.global_permissions + .get(pubkey) + .map(|perms| perms.contains(permission)) + .unwrap_or(false) + } + + /// Check if a user has permission for an object type + pub fn has_object_permission( + &self, + pubkey: &str, + object_type: &str, + permission: &ObjectPermission, + ) -> bool { + self.object_acls + .get(object_type) + .and_then(|acl| acl.permissions.get(pubkey)) + .map(|perms| perms.contains(permission)) + .unwrap_or(false) + } + + /// Check if a user can call a SAL + pub fn can_call_sal(&self, pubkey: &str, sal_name: &str) -> bool { + self.sal_acls + .get(sal_name) + .map(|acl| acl.allowed_callers.contains(&pubkey.to_string())) + .unwrap_or(false) + } + + /// Check if signatories satisfy multi-sig requirements for an object + pub fn check_object_multi_sig( + &self, + signatories: &[String], + object_type: &str, + ) -> bool { + if let Some(acl) = self.object_acls.get(object_type) { + if let Some(multi_sig) = &acl.multi_sig { + return multi_sig.check(signatories, self.global_permissions.len()); + } + } + // No multi-sig requirement + true + } + + /// Check if signatories satisfy multi-sig requirements for a SAL + pub fn check_sal_multi_sig( + &self, + signatories: &[String], + sal_name: &str, + ) -> bool { + if let Some(acl) = self.sal_acls.get(sal_name) { + if let Some(multi_sig) = &acl.multi_sig { + return multi_sig.check(signatories, self.global_permissions.len()); + } + } + // No multi-sig requirement + true + } + + /// Add an admin (only admins can call this) + pub fn add_admin(&mut self, caller: &str, new_admin: String) -> Result<(), String> { + if !self.is_admin(caller) { + return Err("Only admins can add admins".to_string()); + } + if !self.admins.contains(&new_admin) { + self.admins.push(new_admin); + } + Ok(()) + } + + /// Grant a global permission to a user (only admins can call this) + pub fn grant_global_permission( + &mut self, + caller: &str, + pubkey: String, + permission: GlobalPermission, + ) -> Result<(), String> { + if !self.is_admin(caller) { + return Err("Only admins can grant permissions".to_string()); + } + self.global_permissions + .entry(pubkey) + .or_insert_with(Vec::new) + .push(permission); + Ok(()) + } + + /// Grant an object permission to a user (only admins can call this) + pub fn grant_object_permission( + &mut self, + caller: &str, + pubkey: String, + object_type: String, + permission: ObjectPermission, + ) -> Result<(), String> { + if !self.is_admin(caller) { + return Err("Only admins can grant permissions".to_string()); + } + self.object_acls + .entry(object_type) + .or_insert_with(|| ObjectAcl { + permissions: HashMap::new(), + multi_sig: None, + }) + .permissions + .entry(pubkey) + .or_insert_with(Vec::new) + .push(permission); + Ok(()) + } + + /// Grant SAL access to a user (only admins can call this) + pub fn grant_sal_access( + &mut self, + caller: &str, + pubkey: String, + sal_name: String, + ) -> Result<(), String> { + if !self.is_admin(caller) { + return Err("Only admins can grant SAL access".to_string()); + } + self.sal_acls + .entry(sal_name) + .or_insert_with(|| SalAcl { + allowed_callers: Vec::new(), + multi_sig: None, + }) + .allowed_callers + .push(pubkey); + Ok(()) + } + + /// Set multi-sig requirement for an object (only admins can call this) + pub fn set_object_multi_sig( + &mut self, + caller: &str, + object_type: String, + multi_sig: MultiSigRequirement, + ) -> Result<(), String> { + if !self.is_admin(caller) { + return Err("Only admins can set multi-sig requirements".to_string()); + } + self.object_acls + .entry(object_type) + .or_insert_with(|| ObjectAcl { + permissions: HashMap::new(), + multi_sig: None, + }) + .multi_sig = Some(multi_sig); + Ok(()) + } + + /// Set multi-sig requirement for a SAL (only admins can call this) + pub fn set_sal_multi_sig( + &mut self, + caller: &str, + sal_name: String, + multi_sig: MultiSigRequirement, + ) -> Result<(), String> { + if !self.is_admin(caller) { + return Err("Only admins can set multi-sig requirements".to_string()); + } + self.sal_acls + .entry(sal_name) + .or_insert_with(|| SalAcl { + allowed_callers: Vec::new(), + multi_sig: None, + }) + .multi_sig = Some(multi_sig); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_context_creation() { + let ctx = Context::new("Test Context".to_string(), "admin_pk".to_string()); + assert_eq!(ctx.name, "Test Context"); + assert!(ctx.is_admin("admin_pk")); + } + + #[test] + fn test_admin_permissions() { + let mut ctx = Context::new("Test".to_string(), "admin".to_string()); + + // Admin can add another admin + assert!(ctx.add_admin("admin", "admin2".to_string()).is_ok()); + assert!(ctx.is_admin("admin2")); + + // Non-admin cannot add admin + assert!(ctx.add_admin("user1", "admin3".to_string()).is_err()); + } + + #[test] + fn test_global_permissions() { + let mut ctx = Context::new("Test".to_string(), "admin".to_string()); + + // Admin can grant permissions + assert!(ctx.grant_global_permission("admin", "user1".to_string(), GlobalPermission::Read).is_ok()); + assert!(ctx.has_global_permission("user1", &GlobalPermission::Read)); + assert!(!ctx.has_global_permission("user1", &GlobalPermission::Write)); + + // Non-admin cannot grant permissions + assert!(ctx.grant_global_permission("user1", "user2".to_string(), GlobalPermission::Read).is_err()); + } + + #[test] + fn test_object_permissions() { + let mut ctx = Context::new("Test".to_string(), "admin".to_string()); + + // Admin can grant object permissions + assert!(ctx.grant_object_permission("admin", "user1".to_string(), "notes".to_string(), ObjectPermission::Create).is_ok()); + assert!(ctx.has_object_permission("user1", "notes", &ObjectPermission::Create)); + assert!(!ctx.has_object_permission("user1", "notes", &ObjectPermission::Delete)); + } + + #[test] + fn test_sal_permissions() { + let mut ctx = Context::new("Test".to_string(), "admin".to_string()); + + // Admin can grant SAL access + assert!(ctx.grant_sal_access("admin", "user1".to_string(), "launch_vm".to_string()).is_ok()); + assert!(ctx.can_call_sal("user1", "launch_vm")); + assert!(!ctx.can_call_sal("user1", "send_email")); + } + + #[test] + fn test_object_multi_sig_unanimous() { + let mut ctx = Context::new("Test".to_string(), "admin".to_string()); + + assert!(ctx.set_object_multi_sig( + "admin", + "sensitive_data".to_string(), + MultiSigRequirement::Unanimous { + required_signers: vec!["alice".to_string(), "bob".to_string()], + }, + ).is_ok()); + + // Both signers present - should pass + assert!(ctx.check_object_multi_sig(&["alice".to_string(), "bob".to_string()], "sensitive_data")); + + // Only one signer - should fail + assert!(!ctx.check_object_multi_sig(&["alice".to_string()], "sensitive_data")); + } + + #[test] + fn test_sal_multi_sig_threshold() { + let mut ctx = Context::new("Test".to_string(), "admin".to_string()); + + assert!(ctx.set_sal_multi_sig( + "admin", + "launch_vm".to_string(), + MultiSigRequirement::Threshold { + min_signatures: 2, + allowed_signers: Some(vec!["alice".to_string(), "bob".to_string(), "charlie".to_string()]), + }, + ).is_ok()); + + // 2 signatures - should pass + assert!(ctx.check_sal_multi_sig(&["alice".to_string(), "bob".to_string()], "launch_vm")); + + // 1 signature - should fail + assert!(!ctx.check_sal_multi_sig(&["alice".to_string()], "launch_vm")); + } +} diff --git a/lib/models/context/src/rhai.rs b/lib/models/context/src/rhai.rs new file mode 100644 index 0000000..3c747e9 --- /dev/null +++ b/lib/models/context/src/rhai.rs @@ -0,0 +1,327 @@ +use ::rhai::plugin::*; +use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder}; + +use crate::Context; + +// ============================================================================ +// Context Module +// ============================================================================ + +type RhaiContext = Context; + +#[export_module] +mod rhai_context_module { + use super::RhaiContext; + use crate::MultiSigRequirement; + use ::rhai::{Dynamic, EvalAltResult}; + + /// Create a new context with name and initial admin + #[rhai_fn(name = "new_context", return_raw)] + pub fn new_context(name: String, admin: String) -> Result> { + Ok(RhaiContext::new(name, admin)) + } + + /// Set context description + #[rhai_fn(name = "description", return_raw)] + pub fn set_description( + ctx: &mut RhaiContext, + description: String, + ) -> Result> { + ctx.description = Some(description); + Ok(ctx.clone()) + } + + // ========== Admin Management ========== + + /// Check if a user is an admin + #[rhai_fn(name = "is_admin")] + pub fn is_admin(ctx: &mut RhaiContext, pubkey: String) -> bool { + ctx.is_admin(&pubkey) + } + + /// Add an admin (only admins can call this) + #[rhai_fn(name = "add_admin", return_raw)] + pub fn add_admin( + ctx: &mut RhaiContext, + caller: String, + new_admin: String, + ) -> Result> { + ctx.add_admin(&caller, new_admin) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + Ok(ctx.clone()) + } + + // ========== Global Permission Management (RWX) ========== + + /// Grant a global permission to a user (only admins can call this) + #[rhai_fn(name = "grant_global_permission", return_raw)] + pub fn grant_global_permission( + ctx: &mut RhaiContext, + caller: String, + pubkey: String, + permission: String, + ) -> Result> { + let perm = parse_global_permission(&permission)?; + ctx.grant_global_permission(&caller, pubkey, perm) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + Ok(ctx.clone()) + } + + /// Check if a user has a global permission + #[rhai_fn(name = "has_global_permission", return_raw)] + pub fn has_global_permission( + ctx: &mut RhaiContext, + pubkey: String, + permission: String, + ) -> Result> { + let perm = parse_global_permission(&permission)?; + Ok(ctx.has_global_permission(&pubkey, &perm)) + } + + // ========== Object Permission Management ========== + + /// Grant an object permission to a user (only admins can call this) + #[rhai_fn(name = "grant_object_permission", return_raw)] + pub fn grant_object_permission( + ctx: &mut RhaiContext, + caller: String, + pubkey: String, + object_type: String, + permission: String, + ) -> Result> { + let perm = parse_object_permission(&permission)?; + ctx.grant_object_permission(&caller, pubkey, object_type, perm) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + Ok(ctx.clone()) + } + + /// Check if a user has an object permission + #[rhai_fn(name = "has_object_permission", return_raw)] + pub fn has_object_permission( + ctx: &mut RhaiContext, + pubkey: String, + object_type: String, + permission: String, + ) -> Result> { + let perm = parse_object_permission(&permission)?; + Ok(ctx.has_object_permission(&pubkey, &object_type, &perm)) + } + + // ========== SAL Permission Management (Binary) ========== + + /// Grant SAL access to a user (only admins can call this) + #[rhai_fn(name = "grant_sal_access", return_raw)] + pub fn grant_sal_access( + ctx: &mut RhaiContext, + caller: String, + pubkey: String, + sal_name: String, + ) -> Result> { + ctx.grant_sal_access(&caller, pubkey, sal_name) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + Ok(ctx.clone()) + } + + /// Check if a user can call a SAL + #[rhai_fn(name = "can_call_sal")] + pub fn can_call_sal(ctx: &mut RhaiContext, pubkey: String, sal_name: String) -> bool { + ctx.can_call_sal(&pubkey, &sal_name) + } + + // ========== Multi-Sig Management for Objects ========== + + /// Set unanimous multi-sig requirement for an object (only admins can call this) + #[rhai_fn(name = "set_object_multisig_unanimous", return_raw)] + pub fn set_object_multisig_unanimous( + ctx: &mut RhaiContext, + caller: String, + object_type: String, + required_signers: Vec, + ) -> Result> { + let signers = parse_signers(required_signers)?; + ctx.set_object_multi_sig( + &caller, + object_type, + MultiSigRequirement::Unanimous { required_signers: signers }, + ) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + Ok(ctx.clone()) + } + + /// Set threshold multi-sig requirement for an object (only admins can call this) + #[rhai_fn(name = "set_object_multisig_threshold", return_raw)] + pub fn set_object_multisig_threshold( + ctx: &mut RhaiContext, + caller: String, + object_type: String, + min_signatures: i64, + allowed_signers: Vec, + ) -> Result> { + let signers = parse_signers(allowed_signers)?; + ctx.set_object_multi_sig( + &caller, + object_type, + MultiSigRequirement::Threshold { + min_signatures: min_signatures as usize, + allowed_signers: Some(signers), + }, + ) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + Ok(ctx.clone()) + } + + /// Set percentage multi-sig requirement for an object (only admins can call this) + #[rhai_fn(name = "set_object_multisig_percentage", return_raw)] + pub fn set_object_multisig_percentage( + ctx: &mut RhaiContext, + caller: String, + object_type: String, + percentage: f64, + allowed_signers: Vec, + ) -> Result> { + if percentage < 0.0 || percentage > 1.0 { + return Err("Percentage must be between 0.0 and 1.0".into()); + } + let signers = parse_signers(allowed_signers)?; + ctx.set_object_multi_sig( + &caller, + object_type, + MultiSigRequirement::Percentage { + percentage, + allowed_signers: Some(signers), + }, + ) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + Ok(ctx.clone()) + } + + // ========== Multi-Sig Management for SALs ========== + + /// Set unanimous multi-sig requirement for a SAL (only admins can call this) + #[rhai_fn(name = "set_sal_multisig_unanimous", return_raw)] + pub fn set_sal_multisig_unanimous( + ctx: &mut RhaiContext, + caller: String, + sal_name: String, + required_signers: Vec, + ) -> Result> { + let signers = parse_signers(required_signers)?; + ctx.set_sal_multi_sig( + &caller, + sal_name, + MultiSigRequirement::Unanimous { required_signers: signers }, + ) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + Ok(ctx.clone()) + } + + /// Set threshold multi-sig requirement for a SAL (only admins can call this) + #[rhai_fn(name = "set_sal_multisig_threshold", return_raw)] + pub fn set_sal_multisig_threshold( + ctx: &mut RhaiContext, + caller: String, + sal_name: String, + min_signatures: i64, + allowed_signers: Vec, + ) -> Result> { + let signers = parse_signers(allowed_signers)?; + ctx.set_sal_multi_sig( + &caller, + sal_name, + MultiSigRequirement::Threshold { + min_signatures: min_signatures as usize, + allowed_signers: Some(signers), + }, + ) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + Ok(ctx.clone()) + } + + /// Set percentage multi-sig requirement for a SAL (only admins can call this) + #[rhai_fn(name = "set_sal_multisig_percentage", return_raw)] + pub fn set_sal_multisig_percentage( + ctx: &mut RhaiContext, + caller: String, + sal_name: String, + percentage: f64, + allowed_signers: Vec, + ) -> Result> { + if percentage < 0.0 || percentage > 1.0 { + return Err("Percentage must be between 0.0 and 1.0".into()); + } + let signers = parse_signers(allowed_signers)?; + ctx.set_sal_multi_sig( + &caller, + sal_name, + MultiSigRequirement::Percentage { + percentage, + allowed_signers: Some(signers), + }, + ) + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + Ok(ctx.clone()) + } + + // ========== Getters ========== + + #[rhai_fn(name = "get_name")] + pub fn get_name(ctx: &mut RhaiContext) -> String { + ctx.name.clone() + } + + #[rhai_fn(name = "get_description")] + pub fn get_description(ctx: &mut RhaiContext) -> String { + ctx.description.clone().unwrap_or_default() + } +} + +// Helper functions to parse permissions +fn parse_global_permission(permission: &str) -> Result> { + match permission { + "read" => Ok(crate::GlobalPermission::Read), + "write" => Ok(crate::GlobalPermission::Write), + "execute" => Ok(crate::GlobalPermission::Execute), + _ => Err(format!("Invalid global permission: {}", permission).into()), + } +} + +fn parse_object_permission(permission: &str) -> Result> { + match permission { + "create" => Ok(crate::ObjectPermission::Create), + "read" => Ok(crate::ObjectPermission::Read), + "update" => Ok(crate::ObjectPermission::Update), + "delete" => Ok(crate::ObjectPermission::Delete), + "list" => Ok(crate::ObjectPermission::List), + _ => Err(format!("Invalid object permission: {}", permission).into()), + } +} + +fn parse_signers(signers: Vec) -> Result, Box> { + signers + .into_iter() + .map(|d| d.into_string().map_err(|e| format!("Invalid signer: {:?}", e))) + .collect::, _>>() + .map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE))) +} + +impl CustomType for Context { + fn build(mut builder: TypeBuilder) { + builder.with_name("Context"); + } +} + +/// Register the Context module with the Rhai engine +pub fn register_context_module(engine: &mut Engine) { + let module = exported_module!(rhai_context_module); + engine.register_static_module("context", module.into()); + engine.register_type::(); +} + +/// Register Context functions directly on the engine (for global access) +pub fn register_context_functions(engine: &mut Engine) { + engine.register_type::(); + + // Register the module functions + let module = exported_module!(rhai_context_module); + engine.register_global_module(module.into()); +} diff --git a/lib/models/context/src/rhai_old.rs b/lib/models/context/src/rhai_old.rs new file mode 100644 index 0000000..feedee5 --- /dev/null +++ b/lib/models/context/src/rhai_old.rs @@ -0,0 +1,333 @@ +use ::rhai::plugin::*; +use ::rhai::{CustomType, Dynamic, Engine, EvalAltResult, Module, TypeBuilder}; + +use crate::Context; + +// ============================================================================ +// Context Module +// ============================================================================ + +type RhaiContext = Context; + +#[export_module] +mod rhai_context_module { + use super::RhaiContext; + use crate::{GlobalPermission, MultiSigRequirement, ObjectPermission}; + use ::rhai::{Dynamic, EvalAltResult}; + + /// Create a new context with name and initial admin + #[rhai_fn(name = "new_context", return_raw)] + pub fn new_context(name: String, admin: String) -> Result> { + Ok(RhaiContext::new(name, admin)) + } + + /// Set context description + #[rhai_fn(name = "description", return_raw)] + pub fn set_description( + ctx: &mut RhaiContext, + description: String, + ) -> Result> { + ctx.description = Some(description); + Ok(ctx.clone()) + } + + // ========== Global Permission Management ========== + + /// Grant a global permission to a user + #[rhai_fn(name = "grant_permission", return_raw)] + pub fn grant_permission( + ctx: &mut RhaiContext, + pubkey: String, + permission: String, + ) -> Result> { + let perm = match permission.as_str() { + "read" => Permission::Read, + "write" => Permission::Write, + "delete" => Permission::Delete, + "execute" => Permission::Execute, + "admin" => Permission::Admin, + "invite" => Permission::Invite, + _ => return Err(format!("Invalid permission: {}", permission).into()), + }; + ctx.grant_permission(pubkey, perm); + Ok(ctx.clone()) + } + + /// Check if a user has a global permission + #[rhai_fn(name = "has_permission", return_raw)] + pub fn has_permission( + ctx: &mut RhaiContext, + pubkey: String, + permission: String, + ) -> Result> { + let perm = match permission.as_str() { + "read" => Permission::Read, + "write" => Permission::Write, + "delete" => Permission::Delete, + "execute" => Permission::Execute, + "admin" => Permission::Admin, + "invite" => Permission::Invite, + _ => return Err(format!("Invalid permission: {}", permission).into()), + }; + Ok(ctx.has_permission(&pubkey, &perm)) + } + + // ========== Object Permission Management ========== + + /// Grant an object permission to a user + #[rhai_fn(name = "grant_object_permission", return_raw)] + pub fn grant_object_permission( + ctx: &mut RhaiContext, + pubkey: String, + object_type: String, + permission: String, + ) -> Result> { + let perm = parse_resource_permission(&permission)?; + ctx.grant_resource_permission(pubkey, object_type, perm, false); + Ok(ctx.clone()) + } + + /// Check if a user has an object permission + #[rhai_fn(name = "has_object_permission", return_raw)] + pub fn has_object_permission( + ctx: &mut RhaiContext, + pubkey: String, + object_type: String, + permission: String, + ) -> Result> { + let perm = parse_resource_permission(&permission)?; + Ok(ctx.has_resource_permission(&pubkey, &object_type, &perm, false)) + } + + // ========== SAL Permission Management ========== + + /// Grant a SAL permission to a user + #[rhai_fn(name = "grant_sal_permission", return_raw)] + pub fn grant_sal_permission( + ctx: &mut RhaiContext, + pubkey: String, + sal_name: String, + permission: String, + ) -> Result> { + let perm = parse_resource_permission(&permission)?; + ctx.grant_resource_permission(pubkey, sal_name, perm, true); + Ok(ctx.clone()) + } + + /// Check if a user has a SAL permission + #[rhai_fn(name = "has_sal_permission", return_raw)] + pub fn has_sal_permission( + ctx: &mut RhaiContext, + pubkey: String, + sal_name: String, + permission: String, + ) -> Result> { + let perm = parse_resource_permission(&permission)?; + Ok(ctx.has_resource_permission(&pubkey, &sal_name, &perm, true)) + } + + // ========== Multi-Sig Management ========== + + /// Set unanimous multi-sig requirement for an object + #[rhai_fn(name = "set_object_multisig_unanimous", return_raw)] + pub fn set_object_multisig_unanimous( + ctx: &mut RhaiContext, + object_type: String, + required_signers: Vec, + ) -> Result> { + let signers: Result, _> = required_signers + .into_iter() + .map(|d| d.into_string().map_err(|e| format!("Invalid signer: {:?}", e))) + .collect(); + + let signers = signers.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + + ctx.set_multi_sig( + object_type, + MultiSigRequirement::Unanimous { required_signers: signers }, + false, + ); + Ok(ctx.clone()) + } + + /// Set threshold multi-sig requirement for an object + #[rhai_fn(name = "set_object_multisig_threshold", return_raw)] + pub fn set_object_multisig_threshold( + ctx: &mut RhaiContext, + object_type: String, + min_signatures: i64, + allowed_signers: Vec, + ) -> Result> { + let signers: Result, _> = allowed_signers + .into_iter() + .map(|d| d.into_string().map_err(|e| format!("Invalid signer: {:?}", e))) + .collect(); + + let signers = signers.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + + ctx.set_multi_sig( + object_type, + MultiSigRequirement::Threshold { + min_signatures: min_signatures as usize, + allowed_signers: Some(signers), + }, + false, + ); + Ok(ctx.clone()) + } + + /// Set percentage multi-sig requirement for an object + #[rhai_fn(name = "set_object_multisig_percentage", return_raw)] + pub fn set_object_multisig_percentage( + ctx: &mut RhaiContext, + object_type: String, + percentage: f64, + allowed_signers: Vec, + ) -> Result> { + if percentage < 0.0 || percentage > 1.0 { + return Err("Percentage must be between 0.0 and 1.0".into()); + } + + let signers: Result, _> = allowed_signers + .into_iter() + .map(|d| d.into_string().map_err(|e| format!("Invalid signer: {:?}", e))) + .collect(); + + let signers = signers.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + + ctx.set_multi_sig( + object_type, + MultiSigRequirement::Percentage { + percentage, + allowed_signers: Some(signers), + }, + false, + ); + Ok(ctx.clone()) + } + + /// Set unanimous multi-sig requirement for a SAL + #[rhai_fn(name = "set_sal_multisig_unanimous", return_raw)] + pub fn set_sal_multisig_unanimous( + ctx: &mut RhaiContext, + sal_name: String, + required_signers: Vec, + ) -> Result> { + let signers: Result, _> = required_signers + .into_iter() + .map(|d| d.into_string().map_err(|e| format!("Invalid signer: {:?}", e))) + .collect(); + + let signers = signers.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + + ctx.set_multi_sig( + sal_name, + MultiSigRequirement::Unanimous { required_signers: signers }, + true, + ); + Ok(ctx.clone()) + } + + /// Set threshold multi-sig requirement for a SAL + #[rhai_fn(name = "set_sal_multisig_threshold", return_raw)] + pub fn set_sal_multisig_threshold( + ctx: &mut RhaiContext, + sal_name: String, + min_signatures: i64, + allowed_signers: Vec, + ) -> Result> { + let signers: Result, _> = allowed_signers + .into_iter() + .map(|d| d.into_string().map_err(|e| format!("Invalid signer: {:?}", e))) + .collect(); + + let signers = signers.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + + ctx.set_multi_sig( + sal_name, + MultiSigRequirement::Threshold { + min_signatures: min_signatures as usize, + allowed_signers: Some(signers), + }, + true, + ); + Ok(ctx.clone()) + } + + /// Set percentage multi-sig requirement for a SAL + #[rhai_fn(name = "set_sal_multisig_percentage", return_raw)] + pub fn set_sal_multisig_percentage( + ctx: &mut RhaiContext, + sal_name: String, + percentage: f64, + allowed_signers: Vec, + ) -> Result> { + if percentage < 0.0 || percentage > 1.0 { + return Err("Percentage must be between 0.0 and 1.0".into()); + } + + let signers: Result, _> = allowed_signers + .into_iter() + .map(|d| d.into_string().map_err(|e| format!("Invalid signer: {:?}", e))) + .collect(); + + let signers = signers.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))?; + + ctx.set_multi_sig( + sal_name, + MultiSigRequirement::Percentage { + percentage, + allowed_signers: Some(signers), + }, + true, + ); + Ok(ctx.clone()) + } + + // ========== Getters ========== + + #[rhai_fn(name = "get_name")] + pub fn get_name(ctx: &mut RhaiContext) -> String { + ctx.name.clone() + } + + #[rhai_fn(name = "get_description")] + pub fn get_description(ctx: &mut RhaiContext) -> String { + ctx.description.clone().unwrap_or_default() + } +} + +// Helper function to parse resource permissions +fn parse_resource_permission(permission: &str) -> Result> { + match permission { + "create" => Ok(crate::ResourcePermission::Create), + "read" => Ok(crate::ResourcePermission::Read), + "update" => Ok(crate::ResourcePermission::Update), + "delete" => Ok(crate::ResourcePermission::Delete), + "list" => Ok(crate::ResourcePermission::List), + "execute" => Ok(crate::ResourcePermission::Execute), + _ => Err(format!("Invalid resource permission: {}", permission).into()), + } +} + +impl CustomType for Context { + fn build(mut builder: TypeBuilder) { + builder.with_name("Context"); + } +} + +/// Register the Context module with the Rhai engine +pub fn register_context_module(engine: &mut Engine) { + let module = exported_module!(rhai_context_module); + engine.register_static_module("context", module.into()); + engine.register_type::(); +} + +/// Register Context functions directly on the engine (for global access) +pub fn register_context_functions(engine: &mut Engine) { + engine.register_type::(); + + // Register the module functions + let module = exported_module!(rhai_context_module); + engine.register_global_module(module.into()); +}