cratestack_core/
context.rs1mod identity;
6mod principal;
7mod system;
8
9#[cfg(test)]
10mod tests;
11
12use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15
16use crate::error::CoolError;
17use crate::value::Value;
18
19pub use identity::CoolAuthIdentity;
20pub use principal::{PrincipalContext, PrincipalFacet};
21pub use system::SystemContext;
22
23use principal::lookup_value_path_in_map;
24
25#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
26pub struct CoolContext {
27 pub auth: Option<CoolAuthIdentity>,
28 pub principal: Option<PrincipalContext>,
29 pub extensions: BTreeMap<String, Value>,
30 #[serde(skip)]
53 system: bool,
54}
55
56#[derive(Debug, Clone, Copy)]
57pub struct RequestContext<'a> {
58 pub method: &'a str,
59 pub path: &'a str,
60 pub query: Option<&'a str>,
61 pub headers: &'a http::HeaderMap,
62 pub body: &'a [u8],
63}
64
65pub trait AuthProvider: Clone + Send + Sync + 'static {
66 type Error: Into<CoolError> + Send;
67
68 fn authenticate(
69 &self,
70 request: &RequestContext<'_>,
71 ) -> impl ::core::future::Future<Output = Result<CoolContext, Self::Error>> + Send;
72}
73
74impl<F, E> AuthProvider for F
75where
76 F: Clone + Send + Sync + 'static + for<'a> Fn(&'a http::HeaderMap) -> Result<CoolContext, E>,
77 E: Into<CoolError> + Send,
78{
79 type Error = E;
80
81 fn authenticate(
82 &self,
83 request: &RequestContext<'_>,
84 ) -> impl ::core::future::Future<Output = Result<CoolContext, Self::Error>> + Send {
85 let result = (self)(request.headers);
86 ::core::future::ready(result)
87 }
88}
89
90impl CoolContext {
91 pub fn anonymous() -> Self {
92 Self::default()
93 }
94
95 pub fn authenticated(fields: impl IntoIterator<Item = (String, Value)>) -> Self {
96 let fields = fields.into_iter().collect::<BTreeMap<_, _>>();
97 Self {
98 auth: Some(CoolAuthIdentity {
99 fields: fields.clone(),
100 }),
101 principal: Some(PrincipalContext::from_claims(fields)),
102 extensions: BTreeMap::new(),
103 system: false,
104 }
105 }
106
107 pub fn is_authenticated(&self) -> bool {
108 self.auth.is_some() || self.principal.is_some()
109 }
110
111 pub fn is_system(&self) -> bool {
118 self.system
119 }
120
121 pub fn auth_field(&self, name: &str) -> Option<&Value> {
122 if let Some(auth) = self.auth.as_ref()
123 && let Some(value) = auth
124 .fields
125 .get(name)
126 .or_else(|| lookup_value_path_in_map(&auth.fields, name))
127 {
128 return Some(value);
129 }
130
131 self.principal
132 .as_ref()
133 .and_then(|principal| principal.field(name))
134 }
135
136 pub fn from_principal<P: Serialize>(principal: Option<P>) -> Result<Self, CoolError> {
137 let Some(principal) = principal else {
138 return Ok(Self::anonymous());
139 };
140
141 let principal = PrincipalContext::from_principal(principal)?;
142 let auth = principal.as_auth_identity();
143 Ok(Self {
144 auth: Some(auth),
145 principal: Some(principal),
146 extensions: BTreeMap::new(),
147 system: false,
148 })
149 }
150
151 pub fn with_principal(principal: PrincipalContext) -> Self {
152 Self {
153 auth: Some(principal.as_auth_identity()),
154 principal: Some(principal),
155 extensions: BTreeMap::new(),
156 system: false,
157 }
158 }
159
160 pub fn principal_actor_id(&self) -> Option<&str> {
165 let from_facet = self
166 .principal
167 .as_ref()
168 .and_then(|p| p.actor.as_ref())
169 .and_then(|facet| facet.fields.get("id"));
170 let from_claims = self.principal.as_ref().and_then(|p| p.claims.get("id"));
171 let from_auth = self.auth.as_ref().and_then(|auth| auth.fields.get("id"));
172 from_facet
173 .or(from_claims)
174 .or(from_auth)
175 .and_then(|v| match v {
176 Value::String(s) => Some(s.as_str()),
177 _ => None,
178 })
179 }
180
181 pub fn tenant_id(&self) -> Option<&str> {
183 self.principal
184 .as_ref()
185 .and_then(|p| p.tenant.as_ref())
186 .and_then(|facet| facet.fields.get("id"))
187 .and_then(|v| match v {
188 Value::String(s) => Some(s.as_str()),
189 _ => None,
190 })
191 }
192
193 pub fn client_ip(&self) -> Option<&str> {
196 self.extensions.get("client_ip").and_then(|v| match v {
197 Value::String(s) => Some(s.as_str()),
198 _ => None,
199 })
200 }
201
202 pub fn request_id(&self) -> Option<&str> {
205 self.extensions.get("request_id").and_then(|v| match v {
206 Value::String(s) => Some(s.as_str()),
207 _ => None,
208 })
209 }
210
211 pub fn audit_claims_snapshot(&self) -> BTreeMap<String, Value> {
214 self.principal
215 .as_ref()
216 .map(|p| p.claims.clone())
217 .unwrap_or_default()
218 }
219
220 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
224 self.extensions
225 .insert("request_id".to_owned(), Value::String(request_id.into()));
226 self
227 }
228
229 pub fn with_client_ip(mut self, ip: impl Into<String>) -> Self {
234 self.extensions
235 .insert("client_ip".to_owned(), Value::String(ip.into()));
236 self
237 }
238}