Skip to content

Commit

Permalink
feat: multiple securities
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
TheEdward162 committed Jan 7, 2025
1 parent b6e02d5 commit 99cbca0
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 35 deletions.
6 changes: 3 additions & 3 deletions core/core/src/sf_core/map_std_impl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -90,9 +90,9 @@ impl MapStdUnstable for MapStdImpl {
}
}

fn http_call(&mut self, mut params: MapHttpRequest) -> Result<Handle, MapHttpCallError> {
fn http_call(&mut self, mut params: MapHttpRequest, security: MapHttpRequestSecurity) -> Result<Handle, MapHttpCallError> {
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
Expand Down
29 changes: 23 additions & 6 deletions core/core_to_map_std/src/unstable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,11 @@ macro_rules! map_value {
};
}

pub enum HttpRequestSecurity {
FirstValid(Vec<String>),
All(Vec<String>),
}

pub struct HttpRequest {
/// HTTP method - will be used as-is.
pub method: String,
Expand All @@ -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<Vec<u8>>,
/// Security configuration
pub security: Option<String>,
pub body: Option<Vec<u8>>
}
pub struct HttpResponse {
/// Status code of the response.
Expand Down Expand Up @@ -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<Handle, HttpCallError>;
fn http_call(&mut self, params: HttpRequest, security: HttpRequestSecurity) -> Result<Handle, HttpCallError>;
fn http_call_head(&mut self, handle: Handle) -> Result<HttpResponse, HttpCallHeadError>;

// input and output
Expand All @@ -315,6 +318,16 @@ pub trait MapStdUnstable {
// MESSAGES //
//////////////

#[derive(Deserialize)]
#[serde(tag = "kind", content = "ids")]
#[serde(rename_all = "kebab-case")]
enum HttpRequestSecuritySettingMessage {
FirstValid(Vec<String>),
All(Vec<String>),
#[serde(untagged)]
Legacy(Option<String>)
}

define_exchange_map_to_core! {
let state: MapStdUnstable;
enum RequestUnstable {
Expand All @@ -324,7 +337,7 @@ define_exchange_map_to_core! {
url: String,
headers: HeadersMultiMap,
query: MultiMap,
security: Option<String>,
security: HttpRequestSecuritySettingMessage,
body: Option<Vec<u8>>,
} -> enum Response {
Ok {
Expand All @@ -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<String> into Vec<String>, the vec being empty on None
HttpRequestSecuritySettingMessage::Legacy(maybe_v) => HttpRequestSecurity::FirstValid(maybe_v.into_iter().collect()),
});

match handle {
Expand Down
88 changes: 64 additions & 24 deletions core/core_to_map_std/src/unstable/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -384,10 +424,10 @@ pub fn resolve_security(
if let Some(body) = &params.body {
let mut body =
serde_json::from_slice::<serde_json::Value>(body).map_err(|e| {
HttpCallError::InvalidSecurityConfiguration(format!(
format!(
"Failed to parse body: {}",
e
))
)
})?;

let keys = if name.starts_with('/') {
Expand All @@ -397,50 +437,50 @@ 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;
for (key_idx, key) in keys.iter().enumerate() {
if nested.is_array() {
let key_number = match key.parse::<usize>() {
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(),
));
);
}
},
}
Expand Down
1 change: 1 addition & 0 deletions core/interpreter_js/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ mod test {
fn http_call(
&mut self,
_params: map_std::unstable::HttpRequest,
_security: map_std::unstable::HttpRequestSecurity
) -> Result<sf_std::abi::Handle, map_std::unstable::HttpCallError> {
Ok(1)
}
Expand Down
3 changes: 2 additions & 1 deletion core_js/map-std/src/unstable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, AnyValue> but can use { [s in string]: AnyValue }. Typescript go brr.
Expand Down
6 changes: 6 additions & 0 deletions examples/comlinks/src/localhost.provider.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
"id": "basic_auth",
"type": "http",
"scheme": "basic"
},
{
"id": "authic_base",
"type": "apiKey",
"in": "header",
"name": "X-API-KEY"
}
]
}
2 changes: 2 additions & 0 deletions examples/comlinks/src/wasm-sdk.example.localhost.map.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
2 changes: 1 addition & 1 deletion examples/python/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 99cbca0

Please sign in to comment.