implement end to end access control rhai example

This commit is contained in:
Timur Gordon
2025-06-24 19:23:06 +02:00
parent aa4712b8af
commit b980f0d8c1
35 changed files with 1068 additions and 1000 deletions

View File

@@ -1,29 +0,0 @@
[package]
name = "rhailib-examples"
version = "0.1.0"
edition = "2021"
publish = false # This is a package of examples, not meant to be published
[dependencies]
# Local Rhailib crates
rhai_client = { path = "../src/client" }
# External dependencies
rhai = "1.18.0"
tokio = { version = "1", features = ["full"] }
log = "0.4"
env_logger = "0.10"
serde_json = "1.0"
chrono = "0.4"
[[bin]]
name = "example_math_worker"
path = "example_math_worker.rs"
[[bin]]
name = "example_string_worker"
path = "example_string_worker.rs"
[[bin]]
name = "dedicated_reply_queue_demo"
path = "dedicated_reply_queue_demo.rs"

View File

@@ -0,0 +1,45 @@
let private_object = new_object()
.title("Alice's Private Object")
.description("This object can only be seen and modified by Alice")
.save_object();
let object_shared_with_bob = new_object()
.title("Alice's Shared Object")
.description("This object can be seen by Bob but modified only by Alice")
.save_object();
let new_access = new_access()
.object_id(object_shared_with_bob.id())
.circle_public_key("bob_pk")
.save_access();
let book_private = new_book()
.title("Alice's private book")
.description("This book is prive to Alice")
.save_book();
let slides_shared = new_slides()
.title("Alice's shared slides")
.description("These slides, despite being in a private collection, are shared with Bob")
.save_slides();
let new_access = new_access()
.object_id(slides_shared.id)
.circle_public_key("bob_pk")
.save_access();
let collection_private = new_collection()
.title("Alice's private collection")
.description("This collection is only visible to Alice")
.add_book(book_private.id)
.add_slides(slides_shared.id)
.save_collection();
let collection_shared = new_collection()
.title("Alice's shared collection")
.description("This collection is shared with Bob")
.save_collection();

View File

@@ -1,6 +0,0 @@
// auth_script.rhai
// This script calls a custom registered function 'check_permission'
// and passes the CALLER_PUBLIC_KEY to it.
// CALLER_PUBLIC_KEY is injected into the script's scope by the rhailib_worker.
check_permission(CALLER_PUBLIC_KEY)

View File

@@ -0,0 +1,16 @@
let private_object = new_object()
.title("Alice's Private Object")
.description("This object can only be seen and modified by Alice")
.save_object();
let object_shared_with_bob = new_object()
.title("Alice's Shared Collection")
.description("This object can be seen by Bob but modified only by Alice")
.save_object();
let new_access = new_access()
.object_id(object_shared_with_bob.id())
.circle_public_key("bob_pk")
.save_access();

View File

@@ -0,0 +1,16 @@
let private_object = new_object()
.title("Alice's Private Object")
.description("This object can only be seen and modified by Alice")
.save_object();
let object_shared_with_bob = new_object()
.title("Alice's Shared Collection")
.description("This object can be seen by Bob but modified only by Alice")
.save_object();
let new_access = new_access()
.object_id(object_shared_with_bob.id())
.circle_public_key("bob_pk")
.save_access();

View File

@@ -1,137 +1,84 @@
use rhai::{Engine, EvalAltResult};
use rhai_client::RhaiClient;
use rhai_client::RhaiClientBuilder;
use rhailib_engine::create_heromodels_engine;
use rhailib_worker::spawn_rhai_worker;
use std::{fs, path::Path, time::Duration};
use tokio::sync::mpsc;
use uuid::Uuid;
// Custom Rhai function for authorization
// It takes the caller's public key as an argument.
fn check_permission(caller_pk: String) -> Result<String, Box<EvalAltResult>> {
log::info!("check_permission called with PK: {}", caller_pk);
if caller_pk == "admin_pk" {
Ok("Access Granted: Welcome Admin!".to_string())
} else if caller_pk == "user_pk" {
Ok("Limited Access: Welcome User!".to_string())
} else {
Ok(format!("Access Denied: Unknown public key '{}'", caller_pk))
}
}
const ALICE_ID: &str = "alice_pk";
const BOB_ID: &str = "bob_pk";
const CHARLIE_ID: &str = "charlie_pk";
const REDIS_URL: &str = "redis://127.0.0.1/";
const DB_DIRECTORY: &str = "./db";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
let redis_url = "redis://127.0.0.1/";
let worker_circle_pk = "auth_worker_circle".to_string();
// 1. Create a Rhai engine and register custom functionality
let mut engine = Engine::new();
engine.register_fn("check_permission", check_permission);
log::info!("Custom 'check_permission' function registered with Rhai engine.");
let mut engine = rhailib_engine::create_heromodels_engine();
// 2. Spawn the Rhai worker
let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
let worker_handle = tokio::spawn(spawn_rhai_worker(
0, // worker_id
worker_circle_pk.clone(),
ALICE_ID.to_string(),
DB_DIRECTORY.to_string(),
engine,
redis_url.to_string(),
REDIS_URL.to_string(),
shutdown_rx,
false, // use_sentinel
));
log::info!("Rhai worker spawned for circle: {}", worker_circle_pk);
log::info!("Rhai worker spawned for circle: {}", ALICE_ID);
// Give the worker a moment to start up
tokio::time::sleep(Duration::from_secs(1)).await;
// 3. Create a Rhai client
let client = RhaiClient::new(redis_url)?;
log::info!("Rhai client created.");
// Alice populates her rhai worker
let client_alice = RhaiClientBuilder::new()
.redis_url(REDIS_URL)
.caller_id(ALICE_ID)
.build()
.unwrap();
// 4. Load the Rhai script content
let script_path_str = "examples/end_to_end/auth_script.rhai"; // Relative to Cargo.toml / rhailib root
let script_content = match fs::read_to_string(script_path_str) {
Ok(content) => content,
Err(e) => {
log::error!("Failed to read script file '{}': {}", script_path_str, e);
// Attempt to read from an alternative path if run via `cargo run --example`
// where current dir might be the crate root.
let alt_script_path = Path::new(file!())
.parent()
.unwrap()
.join("auth_script.rhai");
log::info!("Attempting alternative script path: {:?}", alt_script_path);
fs::read_to_string(&alt_script_path)?
}
};
log::info!("Loaded script content from '{}'", script_path_str);
client_alice.new_play_request()
.recipient_id(&ALICE_ID)
.script_path("examples/end_to_end/alice.rhai")
.timeout(Duration::from_secs(10))
.await_response().await.unwrap();
log::info!("Alice's database populated.");
// Define different caller public keys
let admin_caller_pk = "admin_pk".to_string();
let user_caller_pk = "user_pk".to_string();
let unknown_caller_pk = "unknown_pk".to_string();
// Bob queries Alice's rhai worker
let client_bob = RhaiClientBuilder::new()
.redis_url(REDIS_URL)
.caller_id(BOB_ID)
.build()
.unwrap();
client_bob.new_play_request()
.recipient_id(&ALICE_ID)
.script_path("examples/end_to_end/bob.rhai")
.timeout(Duration::from_secs(10))
.await_response().await.unwrap();
log::info!("Bob's query to Alice's database completed.");
let callers = vec![
("Admin", admin_caller_pk),
("User", user_caller_pk),
("Unknown", unknown_caller_pk),
];
for (caller_name, caller_pk) in callers {
let task_id = Uuid::new_v4().to_string();
log::info!(
"Submitting script for caller '{}' (PK: {}) with task_id: {}",
caller_name,
caller_pk,
task_id
);
match client
.submit_script_and_await_result(
&worker_circle_pk,
task_id.clone(), // task_id (UUID) first
script_content.clone(), // script_content second
Duration::from_secs(10),
Some(caller_pk.clone()), // This is the CALLER_PUBLIC_KEY
)
.await
{
Ok(details) => {
log::info!(
"Task {} for caller '{}' (PK: {}) completed. Status: {}, Output: {:?}, Error: {:?}",
task_id,
caller_name,
caller_pk,
details.status,
details.output,
details.error
);
// Basic assertion for expected output
if caller_pk == "admin_pk" {
assert_eq!(
details.output,
Some("Access Granted: Welcome Admin!".to_string())
);
} else if caller_pk == "user_pk" {
assert_eq!(
details.output,
Some("Limited Access: Welcome User!".to_string())
);
}
}
Err(e) => {
log::error!(
"Task {} for caller '{}' (PK: {}) failed: {}",
task_id,
caller_name,
caller_pk,
e
);
}
}
tokio::time::sleep(Duration::from_millis(100)).await; // Small delay between submissions
}
// Charlie queries Alice's rhai worker
let client_charlie = RhaiClientBuilder::new()
.redis_url(REDIS_URL)
.caller_id(CHARLIE_ID)
.build()
.unwrap();
client_charlie.new_play_request()
.recipient_id(&ALICE_ID)
.script_path("examples/end_to_end/charlie.rhai")
.timeout(Duration::from_secs(10))
.await_response().await.unwrap();
log::info!("Charlie's query to Alice's database completed.");
// 5. Shutdown the worker (optional, could also let it run until program exits)
log::info!("Signaling worker to shutdown...");

View File

@@ -0,0 +1,16 @@
let private_object = new_object()
.title("Alice's Private Object")
.description("This object can only be seen and modified by Alice")
.save_object();
let object_shared_with_bob = new_object()
.title("Alice's Shared Collection")
.description("This object can be seen by Bob but modified only by Alice")
.save_object();
let new_access = new_access()
.object_id(object_shared_with_bob.id())
.circle_public_key("bob_pk")
.save_access();