archive / remove access control from everything

This commit is contained in:
Timur Gordon 2025-08-06 14:26:51 +02:00
parent 6d271068fc
commit c37be2dfcc
2 changed files with 380 additions and 28 deletions

380
src/macros/_archive/lib.rs Normal file
View File

@ -0,0 +1,380 @@
//! # Rhai Authorization Crate
//! This crate provides authorization mechanisms for Rhai functions, particularly those interacting with a database.
//! It includes helper functions for authorization checks and macros to simplify the registration
//! of authorized Rhai functions.
//! ## Features:
//! - `is_super_admin`: Checks if a caller (identified by a public key) is a super admin.
//! - `can_access_resource`: Checks if a caller has specific access rights to a resource, using a database connection.
//! - `get_caller_public_key`: Helper to extract `CALLER_ID` from the Rhai `NativeCallContext`.
//! - `id_from_i64_to_u32`: Helper to convert `i64` Rhai IDs to `u32` Rust IDs.
//! - `register_authorized_get_by_id_fn!`: Macro to register a Rhai function that retrieves a single item by ID, with authorization checks.
//! - `register_authorized_list_fn!`: Macro to register a Rhai function that lists multiple items, filtering them based on authorization.
//! ## Usage:
//! 1. Use the macros to register your Rhai functions, providing a database connection (`Arc<OurDB>`) and necessary type/name information.
//! 2. The macros internally use `can_access_resource` for authorization checks.
//! 3. Ensure `CALLER_ID` is set in the Rhai engine's scope before calling authorized functions.
use rhai::{EvalAltResult, Position};
use std::convert::TryFrom;
/// Extracts the `CALLER_ID` string constant from the Rhai `NativeCallContext`.
/// This key is used to identify the caller for authorization checks.
/// It first checks the current `Scope` and then falls back to the global constants cache.
///
/// # Arguments
/// * `context`: The Rhai `NativeCallContext` of the currently executing function.
///
/// Converts an `i64` (common Rhai integer type) to a `u32` (common Rust ID type).
///
/// # Arguments
/// * `id_i64`: The `i64` value to convert.
///
/// # Errors
/// Returns `Err(EvalAltResult::ErrorMismatchDataType)` if the `i64` value cannot be represented as a `u32`.
pub fn id_from_i64_to_u32(id_i64: i64) -> Result<u32, Box<EvalAltResult>> {
u32::try_from(id_i64).map_err(|_| {
Box::new(EvalAltResult::ErrorMismatchDataType(
"u32".to_string(),
format!("i64 value ({}) that cannot be represented as u32", id_i64),
Position::NONE,
))
})
}
/// Extracts the `CALLER_ID` string constant from the Rhai `NativeCallContext`'s tag.
/// This key is used to identify the caller for authorization checks.
/// Macro to register a Rhai function that retrieves a single resource by its ID, with authorization.
///
/// The macro handles:
/// - Argument parsing (ID).
/// - Caller identification via `CALLER_ID`.
/// - Authorization check using `AccessControlService::can_access_resource`.
/// - Database call to fetch the resource.
/// - Error handling for type mismatches, authorization failures, DB errors, and not found errors.
///
/// # Arguments
/// * `module`: Mutable reference to the Rhai `Module`.
/// * `db_clone`: Cloned `Arc<Db>` for database access.
/// * `acs_clone`: Cloned `Arc<AccessControlService>`.
/// * `rhai_fn_name`: String literal for the Rhai function name (e.g., "get_collection").
/// * `resource_type_str`: String literal for the resource type (e.g., "Collection"), used in authorization checks and error messages.
/// * `db_method_name`: Identifier for the database method to call (e.g., `get_by_id`).
/// * `id_arg_type`: Rust type of the ID argument in Rhai (e.g., `i64`).
/// * `id_rhai_type_name`: String literal for the Rhai type name of the ID (e.g., "i64"), for error messages.
/// * `id_conversion_fn`: Path to a function converting `id_arg_type` to `actual_id_type` (e.g., `id_from_i64_to_u32`).
/// * `actual_id_type`: Rust type of the ID used in the database (e.g., `u32`).
/// * `rhai_return_rust_type`: Rust type of the resource returned by the DB and Rhai function (e.g., `RhaiCollection`).
#[macro_export]
macro_rules! register_authorized_get_by_id_fn {
(
module: $module:expr,
rhai_fn_name: $rhai_fn_name:expr, // String literal for the Rhai function name (e.g., "get_collection")
resource_type_str: $resource_type_str:expr, // String literal for the resource type (e.g., "Collection")
rhai_return_rust_type: $rhai_return_rust_type:ty // Rust type of the resource returned (e.g., `RhaiCollection`)
) => {
FuncRegistration::new($rhai_fn_name).set_into_module(
$module,
move |context: rhai::NativeCallContext,
id_val: i64|
-> Result<$rhai_return_rust_type, Box<EvalAltResult>> {
let actual_id: u32 = $crate::id_from_i64_to_u32(id_val)?;
// Inlined logic to get caller public key
let tag_map = context
.tag()
.and_then(|tag| tag.read_lock::<rhai::Map>())
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"Context tag must be a Map.".into(),
context.position(),
))
})?;
let pk_dynamic = tag_map.get("CALLER_ID").ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"'CALLER_ID' not found in context tag Map.".into(),
context.position(),
))
})?;
let db_path = tag_map.get("DB_PATH").ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"'DB_PATH' not found in context tag Map.".into(),
context.position(),
))
})?;
let db_path = db_path.clone().into_string()?;
let circle_pk = tag_map.get("CONTEXT_ID").ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
"'CONTEXT_ID' not found in context tag Map.".into(),
context.position(),
))
})?;
let circle_pk = circle_pk.clone().into_string()?;
let db_path = format!("{}/{}", db_path, circle_pk);
let db = Arc::new(OurDB::new(db_path, false).expect("Failed to create DB"));
let caller_pk_str = pk_dynamic.clone().into_string()?;
println!("Checking access for public key: {}", caller_pk_str);
if circle_pk != caller_pk_str {
// Use the standalone can_access_resource function from heromodels
let has_access = heromodels::models::access::access::can_access_resource(
db.clone(),
&caller_pk_str,
actual_id,
$resource_type_str,
);
if !has_access {
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Access denied for public key: {}", caller_pk_str).into(),
context.position(),
)));
}
}
let result = db
.collection::<$rhai_return_rust_type>()
.unwrap()
.get_by_id(actual_id)
.map_err(|e| {
println!(
"Database error fetching {} with ID: {}",
$resource_type_str, actual_id
);
Box::new(EvalAltResult::ErrorRuntime(
format!("Database error fetching {}: {:?}", $resource_type_str, e)
.into(),
context.position(),
))
})?
.ok_or_else(|| {
Box::new(EvalAltResult::ErrorRuntime(
format!(
"Database error fetching {} with ID: {}",
$resource_type_str, actual_id
)
.into(),
context.position(),
))
})?;
Ok(result)
},
);
};
}
// Macro to register a Rhai function that retrieves a single resource by its ID, with authorization.
#[macro_export]
macro_rules! register_authorized_create_by_id_fn {
(
module: $module:expr,
rhai_fn_name: $rhai_fn_name:expr, // String literal for the Rhai function name (e.g., "get_collection")
resource_type_str: $resource_type_str:expr, // String literal for the resource type (e.g., "Collection")
rhai_return_rust_type: $rhai_return_rust_type:ty // Rust type of the resource returned (e.g., `RhaiCollection`)
) => {
FuncRegistration::new($rhai_fn_name).set_into_module(
$module,
move |context: rhai::NativeCallContext, object: $rhai_return_rust_type| -> Result<$rhai_return_rust_type, Box<EvalAltResult>> {
// Inlined logic to get caller public key
let tag_map = context
.tag()
.and_then(|tag| tag.read_lock::<rhai::Map>())
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
let pk_dynamic = tag_map.get("CALLER_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_ID' not found in context tag Map.".into(), context.position())))?;
let db_path = tag_map.get("DB_PATH")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'DB_PATH' not found in context tag Map.".into(), context.position())))?;
let db_path = db_path.clone().into_string()?;
let circle_pk = tag_map.get("CONTEXT_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CONTEXT_ID' not found in context tag Map.".into(), context.position())))?;
let circle_pk = circle_pk.clone().into_string()?;
let db_path = format!("{}/{}", db_path, circle_pk);
let db = Arc::new(OurDB::new(db_path, false).expect("Failed to create DB"));
let caller_pk_str = pk_dynamic.clone().into_string()?;
if circle_pk != caller_pk_str {
let is_circle_member = heromodels::models::access::access::is_circle_member(
db.clone(),
&caller_pk_str,
);
if !is_circle_member {
// TODO: check if caller pk is member of circle
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Insufficient authorization. Caller public key {} does not match circle public key {}", caller_pk_str, circle_pk).into(),
context.position(),
)));
}
}
let result = db.set(&object).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Database error creating {}: {:?}", $resource_type_str, e).into(),
context.position(),
))
})?;
Ok(result.1)
},
);
};
}
// Macro to register a Rhai function that retrieves a single resource by its ID, with authorization.
#[macro_export]
macro_rules! register_authorized_delete_by_id_fn {
(
module: $module:expr,
rhai_fn_name: $rhai_fn_name:expr, // String literal for the Rhai function name (e.g., "get_collection")
resource_type_str: $resource_type_str:expr, // String literal for the resource type (e.g., "Collection")
rhai_return_rust_type: $rhai_return_rust_type:ty // Rust type of the resource returned (e.g., `RhaiCollection`)
) => {
FuncRegistration::new($rhai_fn_name).set_into_module(
$module,
move |context: rhai::NativeCallContext, id_val: i64| -> Result<(), Box<EvalAltResult>> {
let actual_id: u32 = $crate::id_from_i64_to_u32(id_val)?;
// Inlined logic to get caller public key
let tag_map = context
.tag()
.and_then(|tag| tag.read_lock::<rhai::Map>())
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
let pk_dynamic = tag_map.get("CALLER_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_ID' not found in context tag Map.".into(), context.position())))?;
let db_path = tag_map.get("DB_PATH")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'DB_PATH' not found in context tag Map.".into(), context.position())))?;
let db_path = db_path.clone().into_string()?;
let circle_pk = tag_map.get("CONTEXT_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CONTEXT_ID' not found in context tag Map.".into(), context.position())))?;
let circle_pk = circle_pk.clone().into_string()?;
let db_path = format!("{}/{}", db_path, circle_pk);
let db = Arc::new(OurDB::new(db_path, false).expect("Failed to create DB"));
let caller_pk_str = pk_dynamic.clone().into_string()?;
if circle_pk != caller_pk_str {
let is_circle_member = heromodels::models::access::access::is_circle_member(
db.clone(),
&caller_pk_str,
);
if !is_circle_member {
// TODO: check if caller pk is member of circle
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Insufficient authorization. Caller public key {} does not match circle public key {}", caller_pk_str, circle_pk).into(),
context.position(),
)));
}
}
let result = db
.collection::<$rhai_return_rust_type>()
.unwrap()
.delete_by_id(actual_id)
.map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Database error deleting {}: {:?}", $resource_type_str, e).into(),
context.position(),
))
})?;
Ok(())
},
);
};
}
/// Macro to register a Rhai function that lists all resources of a certain type, with authorization.
///
/// The macro handles:
/// - Caller identification via `CALLER_ID`.
/// - Fetching all items of a specific type from the database.
/// - Filtering the items based on the standalone `can_access_resource` function for each item.
/// - Wrapping the authorized items in a specified collection type (e.g., `RhaiCollectionArray`).
/// - Error handling for DB errors during fetch or authorization checks.
///
/// # Arguments
/// * `module`: Mutable reference to the Rhai `Module`.
/// * `rhai_fn_name`: String literal for the Rhai function name (e.g., "list_collections").
/// * `resource_type_str`: String literal for the resource type (e.g., "Collection"), used in authorization checks.
/// * `rhai_return_rust_type`: Rust type of the resource item (e.g., `RhaiCollection`).
/// * `item_id_accessor`: Identifier for the method on `rhai_return_rust_type` that returns its ID (e.g., `id`).
/// * `rhai_return_wrapper_type`: Rust type that wraps a `Vec` of `rhai_return_rust_type` for Rhai (e.g., `RhaiCollectionArray`).
#[macro_export]
macro_rules! register_authorized_list_fn {
(
module: $module:expr,
rhai_fn_name: $rhai_fn_name:expr,
resource_type_str: $resource_type_str:expr,
rhai_return_rust_type: $rhai_return_rust_type:ty,
rhai_return_wrapper_type: $rhai_return_wrapper_type:ty
) => {
FuncRegistration::new($rhai_fn_name).set_into_module(
$module,
move |context: rhai::NativeCallContext| -> Result<$rhai_return_wrapper_type, Box<EvalAltResult>> {
// Inlined logic to get caller public key
let tag_map = context
.tag()
.and_then(|tag| tag.read_lock::<rhai::Map>())
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("Context tag must be a Map.".into(), context.position())))?;
let pk_dynamic = tag_map.get("CALLER_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CALLER_ID' not found in context tag Map.".into(), context.position())))?;
let caller_pk_str = pk_dynamic.clone().into_string()?;
let db_path = tag_map.get("DB_PATH")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'DB_PATH' not found in context tag Map.".into(), context.position())))?;
let db_path = db_path.clone().into_string()?;
let circle_pk = tag_map.get("CONTEXT_ID")
.ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime("'CONTEXT_ID' not found in context tag Map.".into(), context.position())))?;
let circle_pk = circle_pk.clone().into_string()?;
let db_path = format!("{}/{}", db_path, circle_pk);
let db = Arc::new(OurDB::new(db_path, false).expect("Failed to create DB"));
let all_items: Vec<$rhai_return_rust_type> = db
.collection::<$rhai_return_rust_type>()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("{:?}", e).into(), Position::NONE)))?
.get_all()
.map_err(|e| Box::new(EvalAltResult::ErrorRuntime(format!("{:?}", e).into(), Position::NONE)))?;
let authorized_items: Vec<$rhai_return_rust_type> = all_items
.into_iter()
.filter(|item| {
let resource_id = item.id();
heromodels::models::access::access::can_access_resource(
db.clone(),
&caller_pk_str,
resource_id,
$resource_type_str,
)
})
.collect();
Ok(authorized_items.into())
},
);
};
}

View File

@ -208,20 +208,6 @@ macro_rules! register_authorized_create_by_id_fn {
let caller_pk_str = pk_dynamic.clone().into_string()?;
if circle_pk != caller_pk_str {
let is_circle_member = heromodels::models::access::access::is_circle_member(
db.clone(),
&caller_pk_str,
);
if !is_circle_member {
// TODO: check if caller pk is member of circle
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Insufficient authorization. Caller public key {} does not match circle public key {}", caller_pk_str, circle_pk).into(),
context.position(),
)));
}
}
let result = db.set(&object).map_err(|e| {
Box::new(EvalAltResult::ErrorRuntime(
format!("Database error creating {}: {:?}", $resource_type_str, e).into(),
@ -272,20 +258,6 @@ macro_rules! register_authorized_delete_by_id_fn {
let caller_pk_str = pk_dynamic.clone().into_string()?;
if circle_pk != caller_pk_str {
let is_circle_member = heromodels::models::access::access::is_circle_member(
db.clone(),
&caller_pk_str,
);
if !is_circle_member {
// TODO: check if caller pk is member of circle
return Err(Box::new(EvalAltResult::ErrorRuntime(
format!("Insufficient authorization. Caller public key {} does not match circle public key {}", caller_pk_str, circle_pk).into(),
context.position(),
)));
}
}
let result = db
.collection::<$rhai_return_rust_type>()
.unwrap()