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}