1#![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}