Skip to main content

minco_http/
principal.rs

1use http::HeaderMap;
2use serde::{Deserialize, Serialize};
3use std::collections::{BTreeMap, BTreeSet};
4use thiserror::Error;
5use uuid::Uuid;
6
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub struct Principal {
9    pub subject: String,
10    #[serde(default)]
11    pub permissions: BTreeSet<String>,
12    #[serde(default)]
13    pub claims: BTreeMap<String, String>,
14}
15
16impl Principal {
17    pub fn has_permission(&self, permission: &str) -> bool {
18        self.permissions.contains(permission)
19    }
20}
21
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct RequestMetadata {
24    pub request_id: String,
25    pub principal: Option<Principal>,
26}
27
28pub fn principal_from_headers(
29    headers: &HeaderMap,
30    allow_development_headers: bool,
31) -> Result<RequestMetadata, PrincipalError> {
32    let request_id = headers
33        .get("x-request-id")
34        .and_then(|value| value.to_str().ok())
35        .filter(|value| !value.trim().is_empty())
36        .map_or_else(|| Uuid::now_v7().to_string(), str::to_owned);
37    if !allow_development_headers {
38        return Ok(RequestMetadata {
39            request_id,
40            principal: None,
41        });
42    }
43    let Some(subject) = headers
44        .get("x-minco-subject")
45        .and_then(|value| value.to_str().ok())
46    else {
47        return Ok(RequestMetadata {
48            request_id,
49            principal: None,
50        });
51    };
52    if subject.trim().is_empty() {
53        return Err(PrincipalError::InvalidSubject);
54    }
55    let permissions = headers
56        .get("x-minco-permissions")
57        .and_then(|value| value.to_str().ok())
58        .unwrap_or_default()
59        .split(',')
60        .map(str::trim)
61        .filter(|value| !value.is_empty())
62        .map(str::to_owned)
63        .collect();
64    Ok(RequestMetadata {
65        request_id,
66        principal: Some(Principal {
67            subject: subject.to_owned(),
68            permissions,
69            claims: BTreeMap::new(),
70        }),
71    })
72}
73
74#[derive(Debug, Error, Clone, PartialEq, Eq)]
75pub enum PrincipalError {
76    #[error("principal subject is invalid")]
77    InvalidSubject,
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    #[test]
84    fn development_headers_are_explicitly_opted_in() {
85        let mut headers = HeaderMap::new();
86        headers.insert("x-minco-subject", "user-1".parse().unwrap());
87        headers.insert(
88            "x-minco-permissions",
89            "orders.read,orders.create".parse().unwrap(),
90        );
91        assert!(
92            principal_from_headers(&headers, false)
93                .unwrap()
94                .principal
95                .is_none()
96        );
97        let principal = principal_from_headers(&headers, true)
98            .unwrap()
99            .principal
100            .unwrap();
101        assert!(principal.has_permission("orders.create"));
102    }
103}