1use std::collections::BTreeSet;
5
6use async_trait::async_trait;
7
8use crate::{
9 ContextError, PlatformContextProvider, RequestContext, RequestId, RequestMetadata,
10 ScheduledContextProvider, SubjectId, TenantId,
11};
12
13#[derive(Debug, Clone)]
16pub struct StaticContextProvider {
17 pub tenant_id: TenantId,
19 pub subject_id: SubjectId,
21 pub roles: BTreeSet<String>,
23 pub entitlements: BTreeSet<String>,
25 pub locale: String,
27}
28
29impl StaticContextProvider {
30 pub fn new(tenant_id: &str, subject_id: &str) -> Self {
38 Self {
39 tenant_id: tenant_id.parse().expect("StaticContextProvider tenant_id"),
40 subject_id: subject_id
41 .parse()
42 .expect("StaticContextProvider subject_id"),
43 roles: BTreeSet::new(),
44 entitlements: BTreeSet::new(),
45 locale: "en".into(),
46 }
47 }
48
49 pub fn with_roles(mut self, roles: impl IntoIterator<Item = impl Into<String>>) -> Self {
51 self.roles = roles.into_iter().map(Into::into).collect();
52 self
53 }
54
55 pub fn with_entitlements(
57 mut self,
58 entitlements: impl IntoIterator<Item = impl Into<String>>,
59 ) -> Self {
60 self.entitlements = entitlements.into_iter().map(Into::into).collect();
61 self
62 }
63
64 fn context(&self, request_id: RequestId, locale: &str) -> RequestContext {
65 RequestContext {
66 tenant_id: self.tenant_id.clone(),
67 subject_id: self.subject_id.clone(),
68 roles: self.roles.clone(),
69 locale: locale.to_owned(),
70 request_id,
71 entitlements: self.entitlements.clone(),
72 }
73 }
74}
75
76#[async_trait]
77impl PlatformContextProvider for StaticContextProvider {
78 async fn resolve(&self, metadata: &RequestMetadata) -> Result<RequestContext, ContextError> {
79 let locale = metadata
80 .0
81 .get("x-locale")
82 .map(String::as_str)
83 .unwrap_or(&self.locale);
84 let request_id = metadata
85 .0
86 .get("x-request-id")
87 .cloned()
88 .unwrap_or_else(|| format!("static-{}", self.roles.len()))
89 .parse()
90 .map_err(|error: crate::EmptyId| ContextError::Rejected(error.to_string()))?;
91 Ok(self.context(request_id, locale))
92 }
93}
94
95#[async_trait]
96impl ScheduledContextProvider for StaticContextProvider {
97 async fn resolve_scheduled(
98 &self,
99 tenant_id: &TenantId,
100 subject_id: &SubjectId,
101 request_id: &RequestId,
102 locale: &str,
103 ) -> Result<RequestContext, ContextError> {
104 if tenant_id != &self.tenant_id || subject_id != &self.subject_id {
105 return Err(ContextError::Rejected(format!(
106 "static provider only serves {}/{}",
107 self.tenant_id, self.subject_id
108 )));
109 }
110 Ok(self.context(request_id.clone(), locale))
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[tokio::test]
119 async fn static_provider_serves_one_identity() {
120 let provider = StaticContextProvider::new("tenant", "subject").with_roles(["admin"]);
121 let resolved = provider.resolve(&RequestMetadata::default()).await.unwrap();
122 assert_eq!(resolved.tenant_id, "tenant");
123 assert!(resolved.roles.contains("admin"));
124 assert!(provider
125 .resolve_scheduled(
126 &"other".parse().unwrap(),
127 &"subject".parse().unwrap(),
128 &"r".parse().unwrap(),
129 "en",
130 )
131 .await
132 .is_err());
133 }
134}