Skip to content

Commit 141ef1f

Browse files
authored
Feat/multiple securities (#141)
* 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 * "legacy" http security normalization in map-std, so that core input surface doesn't increase
1 parent 0f338ec commit 141ef1f

File tree

8 files changed

+99
-37
lines changed

8 files changed

+99
-37
lines changed

core/core/src/sf_core/map_std_impl/mod.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use map_std::{
55
unstable::{
66
security::{resolve_security, SecurityMap},
77
HttpCallError as MapHttpCallError, HttpCallHeadError as MapHttpCallHeadError,
8-
HttpRequest as MapHttpRequest, HttpResponse as MapHttpResponse, MapStdUnstable, MapValue,
8+
HttpRequest as MapHttpRequest, HttpRequestSecurity as MapHttpRequestSecurity, HttpResponse as MapHttpResponse, MapStdUnstable, MapValue,
99
SetOutputError, TakeContextError,
1010
},
1111
MapStdFull,
@@ -90,9 +90,11 @@ impl MapStdUnstable for MapStdImpl {
9090
}
9191
}
9292

93-
fn http_call(&mut self, mut params: MapHttpRequest) -> Result<Handle, MapHttpCallError> {
93+
fn http_call(&mut self, mut params: MapHttpRequest, security: Option<MapHttpRequestSecurity>) -> Result<Handle, MapHttpCallError> {
9494
let security_map = self.security.as_ref().unwrap();
95-
resolve_security(security_map, &mut params)?;
95+
if let Some(ref security) = security {
96+
resolve_security(security_map, &mut params, security)?;
97+
}
9698

9799
// IDEA: add profile, provider info as well?
98100
params

core/core_to_map_std/src/unstable/mod.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,14 @@ macro_rules! map_value {
182182
};
183183
}
184184

185+
#[derive(Deserialize)]
186+
#[serde(tag = "kind", content = "ids")]
187+
#[serde(rename_all = "kebab-case")]
188+
pub enum HttpRequestSecurity {
189+
FirstValid(Vec<String>),
190+
All(Vec<String>),
191+
}
192+
185193
pub struct HttpRequest {
186194
/// HTTP method - will be used as-is.
187195
pub method: String,
@@ -192,9 +200,7 @@ pub struct HttpRequest {
192200
/// Multiple values with the same key will be repeated in the query string, no joining will be performed.
193201
pub query: MultiMap,
194202
/// Body as bytes.
195-
pub body: Option<Vec<u8>>,
196-
/// Security configuration
197-
pub security: Option<String>,
203+
pub body: Option<Vec<u8>>
198204
}
199205
pub struct HttpResponse {
200206
/// Status code of the response.
@@ -302,7 +308,7 @@ pub trait MapStdUnstable {
302308
fn stream_close(&mut self, handle: Handle) -> std::io::Result<()>;
303309

304310
// http
305-
fn http_call(&mut self, params: HttpRequest) -> Result<Handle, HttpCallError>;
311+
fn http_call(&mut self, params: HttpRequest, security: Option<HttpRequestSecurity>) -> Result<Handle, HttpCallError>;
306312
fn http_call_head(&mut self, handle: Handle) -> Result<HttpResponse, HttpCallHeadError>;
307313

308314
// input and output
@@ -324,7 +330,7 @@ define_exchange_map_to_core! {
324330
url: String,
325331
headers: HeadersMultiMap,
326332
query: MultiMap,
327-
security: Option<String>,
333+
security: Option<HttpRequestSecurity>,
328334
body: Option<Vec<u8>>,
329335
} -> enum Response {
330336
Ok {
@@ -341,9 +347,8 @@ define_exchange_map_to_core! {
341347
url,
342348
headers,
343349
query,
344-
security,
345350
body,
346-
});
351+
}, security);
347352

348353
match handle {
349354
Ok(handle) => Response::Ok {

core/core_to_map_std/src/unstable/security.rs

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use sf_std::{
1111
HeaderName,
1212
};
1313

14-
use super::{HttpCallError, HttpRequest, MapValue, MapValueObject};
14+
use super::{HttpCallError, HttpRequest, HttpRequestSecurity, MapValue, MapValueObject};
1515

1616
pub enum ApiKeyPlacement {
1717
Header,
@@ -319,25 +319,65 @@ pub fn prepare_security_map(
319319
pub fn resolve_security(
320320
security_map: &SecurityMap,
321321
params: &mut HttpRequest,
322+
security: &HttpRequestSecurity
322323
) -> Result<(), HttpCallError> {
323-
let security = match params.security {
324-
None => return Ok(()),
325-
Some(ref security) => security,
326-
};
324+
match security {
325+
HttpRequestSecurity::FirstValid(ref ids) => {
326+
let mut first_error = None;
327+
for id in ids {
328+
match try_resolve_security(security_map, params, id) {
329+
Ok(()) => return Ok(()),
330+
Err(err) => {
331+
if first_error.is_none() {
332+
first_error = Some(err);
333+
}
334+
}
335+
}
336+
}
337+
338+
match first_error {
339+
None => Ok(()),
340+
Some(err) => return Err(HttpCallError::InvalidSecurityConfiguration(
341+
err
342+
))
343+
}
344+
}
345+
HttpRequestSecurity::All(ref ids) => {
346+
let mut all_errors = Vec::new();
347+
for id in ids {
348+
match try_resolve_security(security_map, params, id) {
349+
Ok(()) => (),
350+
Err(err) => all_errors.push(err.to_string())
351+
}
352+
}
327353

328-
let security_config = security_map.get(security.as_str());
354+
if all_errors.len() > 0 {
355+
return Err(HttpCallError::InvalidSecurityConfiguration(
356+
all_errors.join("\n")
357+
))
358+
}
359+
Ok(())
360+
}
361+
}
362+
}
363+
fn try_resolve_security(
364+
security_map: &SecurityMap,
365+
params: &mut HttpRequest,
366+
security: &str
367+
) -> Result<(), String> {
368+
let security_config = security_map.get(security);
329369

330370
match security_config {
331371
None => {
332-
return Err(HttpCallError::InvalidSecurityConfiguration(format!(
372+
return Err(format!(
333373
"Security configuration for {} is missing",
334374
security
335-
)));
375+
));
336376
}
337377
Some(SecurityMapValue::Error(err)) => {
338-
return Err(HttpCallError::InvalidSecurityConfiguration(
378+
return Err(
339379
SecurityMisconfiguredError::format_errors(std::slice::from_ref(err)),
340-
));
380+
);
341381
}
342382
Some(SecurityMapValue::Security(Security::Http(HttpSecurity::Basic {
343383
username,
@@ -384,10 +424,10 @@ pub fn resolve_security(
384424
if let Some(body) = &params.body {
385425
let mut body =
386426
serde_json::from_slice::<serde_json::Value>(body).map_err(|e| {
387-
HttpCallError::InvalidSecurityConfiguration(format!(
427+
format!(
388428
"Failed to parse body: {}",
389429
e
390-
))
430+
)
391431
})?;
392432

393433
let keys = if name.starts_with('/') {
@@ -397,50 +437,50 @@ pub fn resolve_security(
397437
};
398438

399439
if keys.is_empty() {
400-
return Err(HttpCallError::InvalidSecurityConfiguration(format!(
440+
return Err(format!(
401441
"Invalid field name '{}'",
402442
name
403-
)));
443+
));
404444
}
405445

406446
let mut nested = &mut body;
407447
for (key_idx, key) in keys.iter().enumerate() {
408448
if nested.is_array() {
409449
let key_number = match key.parse::<usize>() {
410450
Ok(n) => n,
411-
Err(_) => return Err(HttpCallError::InvalidSecurityConfiguration(format!(
451+
Err(_) => return Err(format!(
412452
"Field value on path '/{}' is an array but provided key cannot be parsed as a number",
413453
&keys[0..=key_idx].join("/")
414-
)))
454+
))
415455
};
416456
nested = &mut nested[key_number];
417457
} else if nested.is_object() {
418458
nested = &mut nested[key];
419459
} else {
420-
return Err(HttpCallError::InvalidSecurityConfiguration(format!(
460+
return Err(format!(
421461
"Field value on path '/{}' must be an object or an array",
422462
&keys[0..=key_idx].join("/")
423-
)));
463+
));
424464
}
425465
}
426466
*nested = serde_json::Value::from(apikey.to_string());
427467

428468
params.body = Some(serde_json::to_vec(&body).map_err(|e| {
429-
HttpCallError::InvalidSecurityConfiguration(format!(
469+
format!(
430470
"Failed to serialize body: {}",
431471
e
432-
))
472+
)
433473
})?);
434474
} else {
435-
return Err(HttpCallError::InvalidSecurityConfiguration(
475+
return Err(
436476
"Api key placement is set to body but the body is empty".to_string(),
437-
));
477+
);
438478
}
439479
}
440480
(ApiKeyPlacement::Body, None) => {
441-
return Err(HttpCallError::InvalidSecurityConfiguration(
481+
return Err(
442482
"Missing body type".to_string(),
443-
));
483+
);
444484
}
445485
},
446486
}

core/interpreter_js/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ mod test {
139139
fn http_call(
140140
&mut self,
141141
_params: map_std::unstable::HttpRequest,
142+
_security: Option<map_std::unstable::HttpRequestSecurity>
142143
) -> Result<sf_std::abi::Handle, map_std::unstable::HttpCallError> {
143144
Ok(1)
144145
}

core_js/map-std/src/unstable.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export type FetchOptions = {
1111
headers?: MultiMap,
1212
query?: MultiMap,
1313
body?: AnyValue,
14-
security?: string,
14+
/** Security configs to apply to this request. Specifying a string is equal to using `first-valid` */
15+
security?: string | { kind: 'first-valid', ids: string[] } | { kind: 'all', ids: string[] },
1516
};
1617

1718
// Can't use Record<string, AnyValue> but can use { [s in string]: AnyValue }. Typescript go brr.
@@ -202,14 +203,19 @@ export function fetch(url: string, options: FetchOptions): HttpRequest {
202203
finalBody = Array.from(bodyBuffer.inner.data);
203204
}
204205

206+
let security = options.security
207+
if (typeof security === "string") {
208+
security = { kind: "first-valid", ids: [security] }
209+
}
210+
205211
const response = messageExchange({
206212
kind: 'http-call',
207213
method: options.method ?? 'GET',
208214
url,
209215
headers,
210216
query: ensureMultimap(options.query ?? {}),
211217
body: finalBody,
212-
security: options.security,
218+
security,
213219
});
214220

215221
if (response.kind === 'ok') {

examples/comlinks/src/localhost.provider.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818
"id": "basic_auth",
1919
"type": "http",
2020
"scheme": "basic"
21+
},
22+
{
23+
"id": "authic_base",
24+
"type": "apiKey",
25+
"in": "header",
26+
"name": "X-API-KEY"
2127
}
2228
]
2329
}

examples/comlinks/src/wasm-sdk.example.localhost.map.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ var Example = ({ input, parameters, services }) => {
2626
'x-custom-header': [parameters.PARAM]
2727
},
2828
security: 'basic_auth',
29+
// security: { kind: "first-valid", ids: ['authic_base', 'basic_auth'] },
30+
// security: { kind: "all", ids: ['authic_base', 'basic_auth'] },
2931
query: {
3032
'foo': ['bar', 'baz'],
3133
'qux': ['2']

examples/python/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def do_GET(self):
3636
{ "id": 1 },
3737
provider = "localhost",
3838
parameters = { "PARAM": "parameter_value" },
39-
security = { "basic_auth": { "username": "username", "password": "password" } }
39+
security = { "basic_auth": { "username": "username", "password": "password" }, "authic_base": { "apikey": "api_key_value" } }
4040
)
4141
print(f"RESULT: {r}")
4242
except PerformError as e:

0 commit comments

Comments
 (0)