From 00f837b814c6c6d69df0018937ffed1aee9bc0b3 Mon Sep 17 00:00:00 2001 From: datron Date: Tue, 30 Jun 2026 19:45:44 +0530 Subject: [PATCH] feat: harden function code by validating return values Signed-off-by: datron --- .../src/api/functions/handlers.rs | 7 +- .../src/validation_functions.rs | 97 ++++++++++++++----- 2 files changed, 78 insertions(+), 26 deletions(-) diff --git a/crates/context_aware_config/src/api/functions/handlers.rs b/crates/context_aware_config/src/api/functions/handlers.rs index 71024c112..7cbdd0c9f 100644 --- a/crates/context_aware_config/src/api/functions/handlers.rs +++ b/crates/context_aware_config/src/api/functions/handlers.rs @@ -77,7 +77,7 @@ async fn create_handler( let handle = rustyscript::tokio::runtime::Handle::current(); let function = req.function.clone(); handle - .spawn_blocking(move || compile_fn(&function)) + .spawn_blocking(move || compile_fn(&function, &req.function_type)) .await .map_err(|e| { log::error!("Function compilation task failed: {:?}", e); @@ -148,13 +148,14 @@ async fn update_handler( let DbConnection(mut conn) = db_conn; let req = request.into_inner(); let f_name: String = params.into_inner().into(); + let function = fetch_function(&f_name, &mut conn, &workspace_context.schema_name)?; // Function Linter Check if let Some(fc) = &req.draft_code { let handle = rustyscript::tokio::runtime::Handle::current(); - let function = fc.clone(); + let function_code = fc.clone(); handle - .spawn_blocking(move || compile_fn(&function)) + .spawn_blocking(move || compile_fn(&function_code, &function.function_type)) .await .map_err(|err| bad_argument!("Invalid function code: {}", err))??; } diff --git a/crates/context_aware_config/src/validation_functions.rs b/crates/context_aware_config/src/validation_functions.rs index d9ffa1823..ad561ae57 100644 --- a/crates/context_aware_config/src/validation_functions.rs +++ b/crates/context_aware_config/src/validation_functions.rs @@ -7,8 +7,14 @@ use superposition_macros::{bad_argument, validation_error}; use service_utils::service::types::{EncryptionKey, WorkspaceContext}; use superposition_types::{ DBConnection, - api::functions::{FunctionExecutionRequest, FunctionExecutionResponse}, - database::models::cac::{FunctionCode, FunctionRuntimeVersion, FunctionType}, + api::functions::{ + ContextValidationTrigger, FunctionEnvironment, FunctionExecutionRequest, + FunctionExecutionResponse, KeyType, + }, + database::models::{ + ChangeReason, + cac::{FunctionCode, FunctionRuntimeVersion, FunctionType}, + }, result as superposition, }; @@ -73,11 +79,12 @@ pub fn run_js_function( args: FunctionExecutionRequest, runtime_version: FunctionRuntimeVersion, ) -> Result)> { - let wrapped_code = generate_wrapped_code(&code.0); + let wrapped_code = generate_wrapped_code(&code); let module = Module::new("function.js", &wrapped_code); let runtime_options = RuntimeOptions { timeout: Duration::from_millis(1500), + // import_provider: Some(Box::new(DenyFileImportProvider::default())), ..Default::default() }; @@ -147,19 +154,14 @@ pub fn execute_fn( run_js_function(code, args.clone(), runtime_version) } -pub fn compile_fn(code_str: &FunctionCode) -> superposition::Result<()> { - let type_check_code = format!( - r#" - {} - - export function typeCheck() {{ - if (typeof execute === "undefined") {{ - throw new Error("execute function is not defined"); - }} - }} - "#, - code_str - ); +pub fn compile_fn( + code_str: &FunctionCode, + fn_type: &FunctionType, +) -> superposition::Result<()> { + // syntax validation, so we do not need the actual secrets and vars + let stubs = "const SECRETS = {};\nconst VARS = {};\n"; + let stubbed_code = FunctionCode(format!("{}\n{}", stubs, code_str)); + let type_check_code = generate_wrapped_code(&stubbed_code); rustyscript::validate(&type_check_code).map_err(|err| { log::error!("Invalid function syntax: {:?}", err); @@ -185,13 +187,62 @@ pub fn compile_fn(code_str: &FunctionCode) -> superposition::Result<()> { let tokio_runtime = runtime.tokio_runtime(); tokio_runtime .block_on(async { - runtime - .call_function_async::<()>( - Some(&module_handle), - "typeCheck", - json_args!(), - ) - .await + let environment = FunctionEnvironment { + context: serde_json::Map::new(), + overrides: serde_json::Map::new(), + }; + match fn_type { + FunctionType::ValueCompute => { + let payload = FunctionExecutionRequest::ValueComputeFunctionRequest { + name: String::new(), + prefix: String::new(), + r#type: superposition_types::api::functions::KeyType::ConfigKey, + environment, + }; + let serde_json::Value::Array(_) = runtime + .call_function_async::( + Some(&module_handle), + "execute", + json_args!(payload), + ) + .await? else { + return Err(rustyscript::Error::Runtime("The value compute function did not return an array".to_string())); + }; + Ok(()) + } + other => { + let payload = match other { + FunctionType::ValueValidation => FunctionExecutionRequest::ValueValidationFunctionRequest { + key: String::new(), + value: serde_json::Value::String(String::new()), + r#type: + KeyType::ConfigKey, + environment, + }, + FunctionType::ContextValidation => FunctionExecutionRequest::ContextValidationFunctionRequest { + environment, + trigger_reason: ContextValidationTrigger::Context, + }, + FunctionType::ChangeReasonValidation => FunctionExecutionRequest::ChangeReasonValidationFunctionRequest { + change_reason: ChangeReason::default(), + }, + _ => { + return Err(rustyscript::Error::Runtime("An invalid situation occurred in the runtime".to_string())) + } + }; + let serde_json::Value::Bool(_) = runtime + .call_function_async::( + Some(&module_handle), + "execute", + json_args!(payload), + ) + .await? else { + return Err(rustyscript::Error::Runtime("The function did not return a boolean".to_string())) + }; + Ok(()) + } + + } }) .map_err(|e| { let err_str = e.to_string();