minco-http 0.4.0

Axum and Tower HTTP conventions, principals, request IDs, limits, and RFC 9457 errors for Minco
Documentation
use http::HeaderMap;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use thiserror::Error;
use uuid::Uuid;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Principal {
    pub subject: String,
    #[serde(default)]
    pub permissions: BTreeSet<String>,
    #[serde(default)]
    pub claims: BTreeMap<String, String>,
}

impl Principal {
    pub fn has_permission(&self, permission: &str) -> bool {
        self.permissions.contains(permission)
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RequestMetadata {
    pub request_id: String,
    pub principal: Option<Principal>,
}

pub fn principal_from_headers(
    headers: &HeaderMap,
    allow_development_headers: bool,
) -> Result<RequestMetadata, PrincipalError> {
    let request_id = headers
        .get("x-request-id")
        .and_then(|value| value.to_str().ok())
        .filter(|value| !value.trim().is_empty())
        .map_or_else(|| Uuid::now_v7().to_string(), str::to_owned);
    if !allow_development_headers {
        return Ok(RequestMetadata {
            request_id,
            principal: None,
        });
    }
    let Some(subject) = headers
        .get("x-minco-subject")
        .and_then(|value| value.to_str().ok())
    else {
        return Ok(RequestMetadata {
            request_id,
            principal: None,
        });
    };
    if subject.trim().is_empty() {
        return Err(PrincipalError::InvalidSubject);
    }
    let permissions = headers
        .get("x-minco-permissions")
        .and_then(|value| value.to_str().ok())
        .unwrap_or_default()
        .split(',')
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(str::to_owned)
        .collect();
    Ok(RequestMetadata {
        request_id,
        principal: Some(Principal {
            subject: subject.to_owned(),
            permissions,
            claims: BTreeMap::new(),
        }),
    })
}

#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum PrincipalError {
    #[error("principal subject is invalid")]
    InvalidSubject,
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn development_headers_are_explicitly_opted_in() {
        let mut headers = HeaderMap::new();
        headers.insert("x-minco-subject", "user-1".parse().unwrap());
        headers.insert(
            "x-minco-permissions",
            "orders.read,orders.create".parse().unwrap(),
        );
        assert!(
            principal_from_headers(&headers, false)
                .unwrap()
                .principal
                .is_none()
        );
        let principal = principal_from_headers(&headers, true)
            .unwrap()
            .principal
            .unwrap();
        assert!(principal.has_permission("orders.create"));
    }
}