Skip to main content

minco_aws_lambda/
lib.rs

1//! Native Lambda HTTP runtime, API Gateway principal mapping and SSM configuration loading.
2#![forbid(unsafe_code)]
3
4use anyhow::{Context, Result};
5use axum::{Router, extract::Request, middleware::Next, response::Response};
6use lambda_http::{RequestExt, request::RequestContext};
7use minco_http::Principal;
8use std::collections::{BTreeMap, BTreeSet};
9
10pub async fn run_router(router: Router) -> Result<()> {
11    lambda_http::run(router)
12        .await
13        .map_err(|error| anyhow::anyhow!("Lambda HTTP runtime failed: {error}"))
14}
15
16pub async fn inject_api_gateway_principal(mut request: Request, next: Next) -> Response {
17    if let Some(principal) = principal_from_request_context(request.request_context_ref()) {
18        request.extensions_mut().insert(principal);
19    }
20    next.run(request).await
21}
22
23#[must_use]
24pub fn principal_from_request_context(context: Option<&RequestContext>) -> Option<Principal> {
25    let RequestContext::ApiGatewayV2(context) = context? else {
26        return None;
27    };
28    let authorizer = context.authorizer.as_ref()?;
29    let value = serde_json::to_value(authorizer).ok()?;
30    let claims = value
31        .pointer("/jwt/claims")
32        .or_else(|| value.get("claims"))?
33        .as_object()?;
34    let subject = claims.get("sub")?.as_str()?.trim();
35    if subject.is_empty() {
36        return None;
37    }
38    let claims = claims
39        .iter()
40        .filter_map(|(key, value)| value.as_str().map(|value| (key.clone(), value.to_owned())))
41        .collect::<BTreeMap<_, _>>();
42    let mut permissions = BTreeSet::new();
43    for claim in ["scope", "permissions"] {
44        if let Some(value) = claims.get(claim) {
45            permissions.extend(
46                value
47                    .split([',', ' '])
48                    .map(str::trim)
49                    .filter(|value| !value.is_empty())
50                    .map(str::to_owned),
51            );
52        }
53    }
54    Some(Principal {
55        subject: subject.to_owned(),
56        permissions,
57        claims,
58    })
59}
60
61pub async fn load_secure_parameter(name: &str) -> Result<String> {
62    if name.trim().is_empty() {
63        anyhow::bail!("SSM parameter name is empty");
64    }
65    let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
66    let response = aws_sdk_ssm::Client::new(&config)
67        .get_parameter()
68        .name(name)
69        .with_decryption(true)
70        .send()
71        .await
72        .with_context(|| format!("failed to load SSM parameter {name}"))?;
73    response
74        .parameter
75        .and_then(|parameter| parameter.value)
76        .filter(|value| !value.is_empty())
77        .with_context(|| format!("SSM parameter {name} has no value"))
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn absent_gateway_context_is_anonymous() {
86        assert!(principal_from_request_context(None).is_none());
87    }
88}