feat(context): add Context model with admin-based ACL system

This commit is contained in:
Timur Gordon
2025-11-20 08:51:52 +01:00
parent 4e3d7a815d
commit 5d8189a653
7 changed files with 1400 additions and 0 deletions

View File

@@ -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",

View File

@@ -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<dyn std::error::Error>> {
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::<i64>(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::<String>(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::<String>(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::<Dynamic>(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::<Dynamic>(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::<Dynamic>(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::<String>(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::<String>(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::<String>(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::<Dynamic>(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(())
}

View File

@@ -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"] }

View File

@@ -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<String, Vec<ObjectPermission>>,
/// Multi-signature requirements (optional)
pub multi_sig: Option<MultiSigRequirement>,
}
/// 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<String>,
/// Multi-signature requirements (optional)
pub multi_sig: Option<MultiSigRequirement>,
}
/// 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<String>,
},
/// 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<Vec<String>>,
},
/// 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<Vec<String>>,
},
}
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));
}
}

View File

@@ -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<String>,
/// List of admin public keys - only admins can modify ACLs
pub admins: Vec<String>,
/// Global permissions (RWX) - what can users do in this context?
/// Maps public key -> list of global permissions
pub global_permissions: HashMap<String, Vec<GlobalPermission>>,
/// Per-object-type ACLs - fine-grained control over data operations
/// Maps object type (e.g., "notes", "events") -> ACL configuration
pub object_acls: HashMap<String, ObjectAcl>,
/// SAL ACLs - binary permission (can call or not)
/// Maps SAL name (e.g., "launch_vm", "send_email") -> ACL configuration
pub sal_acls: HashMap<String, SalAcl>,
}
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"));
}
}

View File

@@ -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<RhaiContext, Box<EvalAltResult>> {
Ok(RhaiContext::new(name, admin))
}
/// Set context description
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(
ctx: &mut RhaiContext,
description: String,
) -> Result<RhaiContext, Box<EvalAltResult>> {
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<RhaiContext, Box<EvalAltResult>> {
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<RhaiContext, Box<EvalAltResult>> {
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<bool, Box<EvalAltResult>> {
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<RhaiContext, Box<EvalAltResult>> {
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<bool, Box<EvalAltResult>> {
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<RhaiContext, Box<EvalAltResult>> {
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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
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<crate::GlobalPermission, Box<EvalAltResult>> {
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<crate::ObjectPermission, Box<EvalAltResult>> {
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<Dynamic>) -> Result<Vec<String>, Box<EvalAltResult>> {
signers
.into_iter()
.map(|d| d.into_string().map_err(|e| format!("Invalid signer: {:?}", e)))
.collect::<Result<Vec<String>, _>>()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(e.into(), rhai::Position::NONE)))
}
impl CustomType for Context {
fn build(mut builder: TypeBuilder<Self>) {
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::<Context>();
}
/// Register Context functions directly on the engine (for global access)
pub fn register_context_functions(engine: &mut Engine) {
engine.register_type::<Context>();
// Register the module functions
let module = exported_module!(rhai_context_module);
engine.register_global_module(module.into());
}

View File

@@ -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<RhaiContext, Box<EvalAltResult>> {
Ok(RhaiContext::new(name, admin))
}
/// Set context description
#[rhai_fn(name = "description", return_raw)]
pub fn set_description(
ctx: &mut RhaiContext,
description: String,
) -> Result<RhaiContext, Box<EvalAltResult>> {
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<RhaiContext, Box<EvalAltResult>> {
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<bool, Box<EvalAltResult>> {
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<RhaiContext, Box<EvalAltResult>> {
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<bool, Box<EvalAltResult>> {
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<RhaiContext, Box<EvalAltResult>> {
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<bool, Box<EvalAltResult>> {
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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
let signers: Result<Vec<String>, _> = 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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
let signers: Result<Vec<String>, _> = 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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
if percentage < 0.0 || percentage > 1.0 {
return Err("Percentage must be between 0.0 and 1.0".into());
}
let signers: Result<Vec<String>, _> = 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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
let signers: Result<Vec<String>, _> = 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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
let signers: Result<Vec<String>, _> = 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<Dynamic>,
) -> Result<RhaiContext, Box<EvalAltResult>> {
if percentage < 0.0 || percentage > 1.0 {
return Err("Percentage must be between 0.0 and 1.0".into());
}
let signers: Result<Vec<String>, _> = 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<crate::ResourcePermission, Box<EvalAltResult>> {
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<Self>) {
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::<Context>();
}
/// Register Context functions directly on the engine (for global access)
pub fn register_context_functions(engine: &mut Engine) {
engine.register_type::<Context>();
// Register the module functions
let module = exported_module!(rhai_context_module);
engine.register_global_module(module.into());
}