From 4540e1bffb7ffcc237867c85b902d27cdcb543e2 Mon Sep 17 00:00:00 2001 From: TheEdward162 Date: Tue, 7 Jan 2025 13:15:08 +0100 Subject: [PATCH 1/3] feat: multiple securities * Implement support for multiple securities * The map can pick between `first-valid` and `all` kinds, which affect whether all specified securities are applied or just the first valid --- core/core/src/sf_core/map_std_impl/mod.rs | 6 +- core/core_to_map_std/src/unstable/mod.rs | 29 ++++-- core/core_to_map_std/src/unstable/security.rs | 88 ++++++++++++++----- core/interpreter_js/src/lib.rs | 1 + core_js/map-std/src/unstable.ts | 3 +- examples/comlinks/src/localhost.provider.json | 6 ++ .../src/wasm-sdk.example.localhost.map.js | 2 + examples/python/__main__.py | 2 +- 8 files changed, 102 insertions(+), 35 deletions(-) diff --git a/core/core/src/sf_core/map_std_impl/mod.rs b/core/core/src/sf_core/map_std_impl/mod.rs index a892d2a2..2995a806 100644 --- a/core/core/src/sf_core/map_std_impl/mod.rs +++ b/core/core/src/sf_core/map_std_impl/mod.rs @@ -5,7 +5,7 @@ use map_std::{ unstable::{ security::{resolve_security, SecurityMap}, HttpCallError as MapHttpCallError, HttpCallHeadError as MapHttpCallHeadError, - HttpRequest as MapHttpRequest, HttpResponse as MapHttpResponse, MapStdUnstable, MapValue, + HttpRequest as MapHttpRequest, HttpRequestSecurity as MapHttpRequestSecurity, HttpResponse as MapHttpResponse, MapStdUnstable, MapValue, SetOutputError, TakeContextError, }, MapStdFull, @@ -90,9 +90,9 @@ impl MapStdUnstable for MapStdImpl { } } - fn http_call(&mut self, mut params: MapHttpRequest) -> Result { + fn http_call(&mut self, mut params: MapHttpRequest, security: MapHttpRequestSecurity) -> Result { let security_map = self.security.as_ref().unwrap(); - resolve_security(security_map, &mut params)?; + resolve_security(security_map, &mut params, &security)?; // IDEA: add profile, provider info as well? params diff --git a/core/core_to_map_std/src/unstable/mod.rs b/core/core_to_map_std/src/unstable/mod.rs index 9925d3c5..ee52d589 100644 --- a/core/core_to_map_std/src/unstable/mod.rs +++ b/core/core_to_map_std/src/unstable/mod.rs @@ -182,6 +182,11 @@ macro_rules! map_value { }; } +pub enum HttpRequestSecurity { + FirstValid(Vec), + All(Vec), +} + pub struct HttpRequest { /// HTTP method - will be used as-is. pub method: String, @@ -192,9 +197,7 @@ pub struct HttpRequest { /// Multiple values with the same key will be repeated in the query string, no joining will be performed. pub query: MultiMap, /// Body as bytes. - pub body: Option>, - /// Security configuration - pub security: Option, + pub body: Option> } pub struct HttpResponse { /// Status code of the response. @@ -302,7 +305,7 @@ pub trait MapStdUnstable { fn stream_close(&mut self, handle: Handle) -> std::io::Result<()>; // http - fn http_call(&mut self, params: HttpRequest) -> Result; + fn http_call(&mut self, params: HttpRequest, security: HttpRequestSecurity) -> Result; fn http_call_head(&mut self, handle: Handle) -> Result; // input and output @@ -315,6 +318,16 @@ pub trait MapStdUnstable { // MESSAGES // ////////////// +#[derive(Deserialize)] +#[serde(tag = "kind", content = "ids")] +#[serde(rename_all = "kebab-case")] +enum HttpRequestSecuritySettingMessage { + FirstValid(Vec), + All(Vec), + #[serde(untagged)] + Legacy(Option) +} + define_exchange_map_to_core! { let state: MapStdUnstable; enum RequestUnstable { @@ -324,7 +337,7 @@ define_exchange_map_to_core! { url: String, headers: HeadersMultiMap, query: MultiMap, - security: Option, + security: HttpRequestSecuritySettingMessage, body: Option>, } -> enum Response { Ok { @@ -341,8 +354,12 @@ define_exchange_map_to_core! { url, headers, query, - security, body, + }, match security { + HttpRequestSecuritySettingMessage::FirstValid(v) => HttpRequestSecurity::FirstValid(v), + HttpRequestSecuritySettingMessage::All(v) => HttpRequestSecurity::All(v), + // Turns Option into Vec, the vec being empty on None + HttpRequestSecuritySettingMessage::Legacy(maybe_v) => HttpRequestSecurity::FirstValid(maybe_v.into_iter().collect()), }); match handle { diff --git a/core/core_to_map_std/src/unstable/security.rs b/core/core_to_map_std/src/unstable/security.rs index 1614bcac..adae01f1 100644 --- a/core/core_to_map_std/src/unstable/security.rs +++ b/core/core_to_map_std/src/unstable/security.rs @@ -11,7 +11,7 @@ use sf_std::{ HeaderName, }; -use super::{HttpCallError, HttpRequest, MapValue, MapValueObject}; +use super::{HttpCallError, HttpRequest, HttpRequestSecurity, MapValue, MapValueObject}; pub enum ApiKeyPlacement { Header, @@ -319,25 +319,65 @@ pub fn prepare_security_map( pub fn resolve_security( security_map: &SecurityMap, params: &mut HttpRequest, + security: &HttpRequestSecurity ) -> Result<(), HttpCallError> { - let security = match params.security { - None => return Ok(()), - Some(ref security) => security, - }; + match security { + HttpRequestSecurity::FirstValid(ref ids) => { + let mut first_error = None; + for id in ids { + match try_resolve_security(security_map, params, id) { + Ok(()) => return Ok(()), + Err(err) => { + if first_error.is_none() { + first_error = Some(err); + } + } + } + } + + match first_error { + None => Ok(()), + Some(err) => return Err(HttpCallError::InvalidSecurityConfiguration( + err + )) + } + } + HttpRequestSecurity::All(ref ids) => { + let mut all_errors = Vec::new(); + for id in ids { + match try_resolve_security(security_map, params, id) { + Ok(()) => (), + Err(err) => all_errors.push(err.to_string()) + } + } - let security_config = security_map.get(security.as_str()); + if all_errors.len() > 0 { + return Err(HttpCallError::InvalidSecurityConfiguration( + all_errors.join("\n") + )) + } + Ok(()) + } + } +} +fn try_resolve_security( + security_map: &SecurityMap, + params: &mut HttpRequest, + security: &str +) -> Result<(), String> { + let security_config = security_map.get(security); match security_config { None => { - return Err(HttpCallError::InvalidSecurityConfiguration(format!( + return Err(format!( "Security configuration for {} is missing", security - ))); + )); } Some(SecurityMapValue::Error(err)) => { - return Err(HttpCallError::InvalidSecurityConfiguration( + return Err( SecurityMisconfiguredError::format_errors(std::slice::from_ref(err)), - )); + ); } Some(SecurityMapValue::Security(Security::Http(HttpSecurity::Basic { username, @@ -384,10 +424,10 @@ pub fn resolve_security( if let Some(body) = ¶ms.body { let mut body = serde_json::from_slice::(body).map_err(|e| { - HttpCallError::InvalidSecurityConfiguration(format!( + format!( "Failed to parse body: {}", e - )) + ) })?; let keys = if name.starts_with('/') { @@ -397,10 +437,10 @@ pub fn resolve_security( }; if keys.is_empty() { - return Err(HttpCallError::InvalidSecurityConfiguration(format!( + return Err(format!( "Invalid field name '{}'", name - ))); + )); } let mut nested = &mut body; @@ -408,39 +448,39 @@ pub fn resolve_security( if nested.is_array() { let key_number = match key.parse::() { Ok(n) => n, - Err(_) => return Err(HttpCallError::InvalidSecurityConfiguration(format!( + Err(_) => return Err(format!( "Field value on path '/{}' is an array but provided key cannot be parsed as a number", &keys[0..=key_idx].join("/") - ))) + )) }; nested = &mut nested[key_number]; } else if nested.is_object() { nested = &mut nested[key]; } else { - return Err(HttpCallError::InvalidSecurityConfiguration(format!( + return Err(format!( "Field value on path '/{}' must be an object or an array", &keys[0..=key_idx].join("/") - ))); + )); } } *nested = serde_json::Value::from(apikey.to_string()); params.body = Some(serde_json::to_vec(&body).map_err(|e| { - HttpCallError::InvalidSecurityConfiguration(format!( + format!( "Failed to serialize body: {}", e - )) + ) })?); } else { - return Err(HttpCallError::InvalidSecurityConfiguration( + return Err( "Api key placement is set to body but the body is empty".to_string(), - )); + ); } } (ApiKeyPlacement::Body, None) => { - return Err(HttpCallError::InvalidSecurityConfiguration( + return Err( "Missing body type".to_string(), - )); + ); } }, } diff --git a/core/interpreter_js/src/lib.rs b/core/interpreter_js/src/lib.rs index 80a680ff..20bbbdd0 100644 --- a/core/interpreter_js/src/lib.rs +++ b/core/interpreter_js/src/lib.rs @@ -139,6 +139,7 @@ mod test { fn http_call( &mut self, _params: map_std::unstable::HttpRequest, + _security: map_std::unstable::HttpRequestSecurity ) -> Result { Ok(1) } diff --git a/core_js/map-std/src/unstable.ts b/core_js/map-std/src/unstable.ts index d294a6de..6970c0c8 100644 --- a/core_js/map-std/src/unstable.ts +++ b/core_js/map-std/src/unstable.ts @@ -11,7 +11,8 @@ export type FetchOptions = { headers?: MultiMap, query?: MultiMap, body?: AnyValue, - security?: string, + /** Security configs to apply to this request. Specifying a string is equal to using `first-valid` */ + security?: string | { kind: 'first-valid', ids: string[] } | { kind: 'all', ids: string[] }, }; // Can't use Record but can use { [s in string]: AnyValue }. Typescript go brr. diff --git a/examples/comlinks/src/localhost.provider.json b/examples/comlinks/src/localhost.provider.json index 2cb7eb2a..01ed42f2 100644 --- a/examples/comlinks/src/localhost.provider.json +++ b/examples/comlinks/src/localhost.provider.json @@ -18,6 +18,12 @@ "id": "basic_auth", "type": "http", "scheme": "basic" + }, + { + "id": "authic_base", + "type": "apiKey", + "in": "header", + "name": "X-API-KEY" } ] } \ No newline at end of file diff --git a/examples/comlinks/src/wasm-sdk.example.localhost.map.js b/examples/comlinks/src/wasm-sdk.example.localhost.map.js index 7e6efe82..b37cc228 100644 --- a/examples/comlinks/src/wasm-sdk.example.localhost.map.js +++ b/examples/comlinks/src/wasm-sdk.example.localhost.map.js @@ -26,6 +26,8 @@ var Example = ({ input, parameters, services }) => { 'x-custom-header': [parameters.PARAM] }, security: 'basic_auth', + // security: { kind: "first-valid", ids: ['authic_base', 'basic_auth'] }, + // security: { kind: "all", ids: ['authic_base', 'basic_auth'] }, query: { 'foo': ['bar', 'baz'], 'qux': ['2'] diff --git a/examples/python/__main__.py b/examples/python/__main__.py index b060b9b9..9e44abff 100644 --- a/examples/python/__main__.py +++ b/examples/python/__main__.py @@ -36,7 +36,7 @@ def do_GET(self): { "id": 1 }, provider = "localhost", parameters = { "PARAM": "parameter_value" }, - security = { "basic_auth": { "username": "username", "password": "password" } } + security = { "basic_auth": { "username": "username", "password": "password" }, "authic_base": { "apikey": "api_key_value" } } ) print(f"RESULT: {r}") except PerformError as e: From 32c07436608cd86141f6803eefd94c032fa1c86e Mon Sep 17 00:00:00 2001 From: TheEdward162 Date: Wed, 15 Jan 2025 22:40:36 +0100 Subject: [PATCH 2/3] feat: multiple securities * move "legacy" http security normalization to map-std, so that core input surface doesn't increase --- core/core/src/sf_core/map_std_impl/mod.rs | 6 ++++-- core/core_to_map_std/src/unstable/mod.rs | 24 ++++++----------------- core_js/map-std/src/unstable.ts | 7 ++++++- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/core/core/src/sf_core/map_std_impl/mod.rs b/core/core/src/sf_core/map_std_impl/mod.rs index 2995a806..466de472 100644 --- a/core/core/src/sf_core/map_std_impl/mod.rs +++ b/core/core/src/sf_core/map_std_impl/mod.rs @@ -90,9 +90,11 @@ impl MapStdUnstable for MapStdImpl { } } - fn http_call(&mut self, mut params: MapHttpRequest, security: MapHttpRequestSecurity) -> Result { + fn http_call(&mut self, mut params: MapHttpRequest, security: Option) -> Result { let security_map = self.security.as_ref().unwrap(); - resolve_security(security_map, &mut params, &security)?; + if let Some(ref security) = security { + resolve_security(security_map, &mut params, security)?; + } // IDEA: add profile, provider info as well? params diff --git a/core/core_to_map_std/src/unstable/mod.rs b/core/core_to_map_std/src/unstable/mod.rs index ee52d589..9bddcf4a 100644 --- a/core/core_to_map_std/src/unstable/mod.rs +++ b/core/core_to_map_std/src/unstable/mod.rs @@ -182,6 +182,9 @@ macro_rules! map_value { }; } +#[derive(Deserialize)] +#[serde(tag = "kind", content = "ids")] +#[serde(rename_all = "kebab-case")] pub enum HttpRequestSecurity { FirstValid(Vec), All(Vec), @@ -305,7 +308,7 @@ pub trait MapStdUnstable { fn stream_close(&mut self, handle: Handle) -> std::io::Result<()>; // http - fn http_call(&mut self, params: HttpRequest, security: HttpRequestSecurity) -> Result; + fn http_call(&mut self, params: HttpRequest, security: Option) -> Result; fn http_call_head(&mut self, handle: Handle) -> Result; // input and output @@ -318,16 +321,6 @@ pub trait MapStdUnstable { // MESSAGES // ////////////// -#[derive(Deserialize)] -#[serde(tag = "kind", content = "ids")] -#[serde(rename_all = "kebab-case")] -enum HttpRequestSecuritySettingMessage { - FirstValid(Vec), - All(Vec), - #[serde(untagged)] - Legacy(Option) -} - define_exchange_map_to_core! { let state: MapStdUnstable; enum RequestUnstable { @@ -337,7 +330,7 @@ define_exchange_map_to_core! { url: String, headers: HeadersMultiMap, query: MultiMap, - security: HttpRequestSecuritySettingMessage, + security: Option, body: Option>, } -> enum Response { Ok { @@ -355,12 +348,7 @@ define_exchange_map_to_core! { headers, query, body, - }, match security { - HttpRequestSecuritySettingMessage::FirstValid(v) => HttpRequestSecurity::FirstValid(v), - HttpRequestSecuritySettingMessage::All(v) => HttpRequestSecurity::All(v), - // Turns Option into Vec, the vec being empty on None - HttpRequestSecuritySettingMessage::Legacy(maybe_v) => HttpRequestSecurity::FirstValid(maybe_v.into_iter().collect()), - }); + }, security); match handle { Ok(handle) => Response::Ok { diff --git a/core_js/map-std/src/unstable.ts b/core_js/map-std/src/unstable.ts index 6970c0c8..4e632651 100644 --- a/core_js/map-std/src/unstable.ts +++ b/core_js/map-std/src/unstable.ts @@ -203,6 +203,11 @@ export function fetch(url: string, options: FetchOptions): HttpRequest { finalBody = Array.from(bodyBuffer.inner.data); } + let security = options.security + if (typeof security === "string") { + security = { kind: "first-valid", ids: [security] } + } + const response = messageExchange({ kind: 'http-call', method: options.method ?? 'GET', @@ -210,7 +215,7 @@ export function fetch(url: string, options: FetchOptions): HttpRequest { headers, query: ensureMultimap(options.query ?? {}), body: finalBody, - security: options.security, + security, }); if (response.kind === 'ok') { From 73ec2da17bda888fd535e69f6224fc8721d897b4 Mon Sep 17 00:00:00 2001 From: TheEdward162 Date: Wed, 15 Jan 2025 22:44:40 +0100 Subject: [PATCH 3/3] fix: core tests --- core/interpreter_js/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/interpreter_js/src/lib.rs b/core/interpreter_js/src/lib.rs index 20bbbdd0..50d55232 100644 --- a/core/interpreter_js/src/lib.rs +++ b/core/interpreter_js/src/lib.rs @@ -139,7 +139,7 @@ mod test { fn http_call( &mut self, _params: map_std::unstable::HttpRequest, - _security: map_std::unstable::HttpRequestSecurity + _security: Option ) -> Result { Ok(1) }