1use std::sync::Arc;
2
3use anyhow::{Result, anyhow, bail};
4use axum::extract::{FromRef, FromRequestParts};
5use axum::http::header::{AUTHORIZATION, HOST};
6use axum::http::request::Parts;
7use axum::http::{HeaderName, StatusCode};
8use base64::Engine;
9use base64::engine::general_purpose::{STANDARD, URL_SAFE, URL_SAFE_NO_PAD};
10use serde_json::Value;
11use serde_json::json;
12
13use crate::runner::ServerState;
14use crate::runtime::TenantRuntime;
15
16pub const DEFAULT_TENANT: &str = "default";
27
28#[derive(Clone)]
29pub struct RoutingConfig {
30 pub resolver: TenantResolver,
31 pub default_tenant: String,
32}
33
34impl RoutingConfig {
35 pub fn from_env() -> Self {
36 Self::from_env_with_default(DEFAULT_TENANT.into())
37 }
38
39 pub fn from_env_with_default(default_tenant: String) -> Self {
40 let default_tenant = std::env::var("DEFAULT_TENANT").unwrap_or(default_tenant);
41 let resolver = std::env::var("TENANT_RESOLVER")
42 .map(|value| TenantResolver::from_str(&value, &default_tenant))
43 .unwrap_or(Ok(TenantResolver::Env))
44 .unwrap_or_else(|err| {
45 tracing::warn!(error = %err, "invalid TENANT_RESOLVER, falling back to env");
46 TenantResolver::Env
47 });
48 Self {
49 resolver,
50 default_tenant,
51 }
52 }
53}
54
55impl Default for RoutingConfig {
56 fn default() -> Self {
57 Self {
58 resolver: TenantResolver::Env,
59 default_tenant: DEFAULT_TENANT.into(),
60 }
61 }
62}
63
64#[derive(Clone)]
65pub enum TenantResolver {
66 Host,
67 Header(HeaderName),
68 Jwt { header: HeaderName, claim: String },
69 Env,
70}
71
72impl TenantResolver {
73 fn from_str(value: &str, _default: &str) -> Result<Self> {
74 match value.to_ascii_lowercase().as_str() {
75 "host" => Ok(Self::Host),
76 "header" => Ok(Self::Header(HeaderName::from_static("x-greentic-tenant"))),
77 "jwt" => Ok(Self::Jwt {
78 header: AUTHORIZATION,
79 claim: "tenant".into(),
80 }),
81 "env" => Ok(Self::Env),
82 other => bail!("unsupported TENANT_RESOLVER `{other}`"),
83 }
84 }
85}
86
87#[derive(Clone)]
88pub struct TenantRouting {
89 resolver: TenantResolver,
90 default_tenant: String,
91}
92
93impl TenantRouting {
94 pub fn new(cfg: RoutingConfig) -> Self {
95 Self {
96 resolver: cfg.resolver,
97 default_tenant: cfg.default_tenant,
98 }
99 }
100
101 pub fn default_tenant(&self) -> &str {
103 &self.default_tenant
104 }
105
106 pub fn resolve(&self, parts: &Parts) -> Result<String> {
107 match &self.resolver {
108 TenantResolver::Env => Ok(self.default_tenant.clone()),
109 TenantResolver::Host => {
110 let host = parts
111 .headers
112 .get(HOST)
113 .and_then(|value| value.to_str().ok())
114 .unwrap_or_default();
115 if host.is_empty() {
116 return Ok(self.default_tenant.clone());
117 }
118 Ok(host
119 .split('.')
120 .next()
121 .map(|segment| segment.to_string())
122 .filter(|segment| !segment.is_empty())
123 .unwrap_or_else(|| self.default_tenant.clone()))
124 }
125 TenantResolver::Header(name) => {
126 let tenant = parts
127 .headers
128 .get(name)
129 .and_then(|value| value.to_str().ok())
130 .filter(|value| !value.is_empty())
131 .map(|value| value.to_string())
132 .unwrap_or_else(|| self.default_tenant.clone());
133 Ok(tenant)
134 }
135 TenantResolver::Jwt { header, claim } => {
136 let token = parts
137 .headers
138 .get(header)
139 .and_then(|value| value.to_str().ok())
140 .and_then(|value| value.strip_prefix("Bearer "))
141 .ok_or_else(|| anyhow!("authorization header missing"))?;
142 let tenant = decode_jwt_claim(token, claim)
143 .unwrap_or_else(|err| {
144 tracing::warn!(error = %err, "failed to decode jwt claim");
145 None
146 })
147 .unwrap_or_else(|| self.default_tenant.clone());
148 Ok(tenant)
149 }
150 }
151 }
152}
153
154fn decode_jwt_claim(token: &str, claim: &str) -> Result<Option<String>> {
155 let payload = token
156 .split('.')
157 .nth(1)
158 .ok_or_else(|| anyhow!("invalid jwt structure"))?;
159 let bytes = URL_SAFE_NO_PAD.decode(payload.as_bytes()).or_else(|_| {
160 let padded = match payload.len() % 4 {
161 2 => Some(format!("{payload}==")),
162 3 => Some(format!("{payload}=")),
163 _ => None,
164 };
165 if let Some(padded) = padded.as_deref() {
166 URL_SAFE
167 .decode(padded.as_bytes())
168 .or_else(|_| STANDARD.decode(padded.as_bytes()))
169 } else {
170 URL_SAFE
171 .decode(payload.as_bytes())
172 .or_else(|_| STANDARD.decode(payload.as_bytes()))
173 }
174 })?;
175 let value: Value = serde_json::from_slice(&bytes)?;
176 Ok(value
177 .get(claim)
178 .and_then(|node| node.as_str())
179 .map(|value| value.to_string()))
180}
181
182pub struct TenantRuntimeHandle {
183 pub tenant: String,
184 pub runtime: Arc<TenantRuntime>,
185}
186
187impl<S> FromRequestParts<S> for TenantRuntimeHandle
188where
189 ServerState: FromRef<S>,
190 S: Send + Sync,
191{
192 type Rejection = (StatusCode, axum::Json<Value>);
193
194 fn from_request_parts(
195 parts: &mut Parts,
196 state: &S,
197 ) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
198 let server_state = ServerState::from_ref(state);
199 async move {
200 let tenant = server_state.routing.resolve(parts).map_err(|err| {
201 (
202 StatusCode::BAD_REQUEST,
203 axum::Json(json!({ "error": err.to_string() })),
204 )
205 })?;
206 let runtime = server_state.active.load_pack(&tenant).ok_or_else(|| {
207 (
208 StatusCode::NOT_FOUND,
209 axum::Json(json!({ "error": "tenant not loaded" })),
210 )
211 })?;
212 Ok(Self { tenant, runtime })
213 }
214 }
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220 use axum::http::Request;
221
222 #[test]
223 fn host_resolver_picks_subdomain() {
224 let routing = TenantRouting::new(RoutingConfig {
225 resolver: TenantResolver::Host,
226 default_tenant: "demo".into(),
227 });
228 let (parts, _) = Request::builder()
229 .uri("http://foo.example.com/webhook")
230 .header(HOST, "foo.example.com")
231 .body(())
232 .unwrap()
233 .into_parts();
234 let tenant = routing.resolve(&parts).unwrap();
235 assert_eq!(tenant, "foo");
236 }
237
238 #[test]
239 fn header_resolver_defaults() {
240 let routing = TenantRouting::new(RoutingConfig {
241 resolver: TenantResolver::Header(HeaderName::from_static("x-tenant")),
242 default_tenant: "demo".into(),
243 });
244 let (parts, _) = Request::builder()
245 .uri("http://localhost")
246 .body(())
247 .unwrap()
248 .into_parts();
249 let tenant = routing.resolve(&parts).unwrap();
250 assert_eq!(tenant, "demo");
251 }
252
253 #[test]
258 fn routing_defaults_to_the_fleet_tenant_not_demo() {
259 assert_eq!(RoutingConfig::default().default_tenant, DEFAULT_TENANT);
260 assert_ne!(RoutingConfig::default().default_tenant, "demo");
261 assert_eq!(DEFAULT_TENANT, "default");
264 }
265
266 #[test]
267 fn from_env_with_default_uses_override() {
268 let expected = std::env::var("DEFAULT_TENANT").unwrap_or_else(|_| "custom".into());
269 let cfg = RoutingConfig::from_env_with_default("custom".into());
270 assert_eq!(cfg.default_tenant, expected);
271 }
272
273 #[test]
274 fn jwt_resolver_reads_tenant_claim() {
275 let routing = TenantRouting::new(RoutingConfig {
276 resolver: TenantResolver::Jwt {
277 header: AUTHORIZATION,
278 claim: "tenant".into(),
279 },
280 default_tenant: "demo".into(),
281 });
282 let payload = STANDARD.encode(br#"{"tenant":"jwt-tenant"}"#);
283 let token = format!("ignored.{payload}.ignored");
284 let (parts, _) = Request::builder()
285 .header(AUTHORIZATION, format!("Bearer {token}"))
286 .body(())
287 .unwrap()
288 .into_parts();
289
290 assert_eq!(routing.resolve(&parts).unwrap(), "jwt-tenant");
291 }
292
293 #[test]
294 fn jwt_resolver_falls_back_on_invalid_payload() {
295 let routing = TenantRouting::new(RoutingConfig {
296 resolver: TenantResolver::Jwt {
297 header: AUTHORIZATION,
298 claim: "tenant".into(),
299 },
300 default_tenant: "demo".into(),
301 });
302 let (parts, _) = Request::builder()
303 .header(AUTHORIZATION, "Bearer invalid.token.payload")
304 .body(())
305 .unwrap()
306 .into_parts();
307
308 assert_eq!(routing.resolve(&parts).unwrap(), "demo");
309 }
310
311 #[test]
312 fn jwt_resolver_requires_bearer_prefix() {
313 let routing = TenantRouting::new(RoutingConfig {
314 resolver: TenantResolver::Jwt {
315 header: AUTHORIZATION,
316 claim: "tenant".into(),
317 },
318 default_tenant: "demo".into(),
319 });
320 let (parts, _) = Request::builder().body(()).unwrap().into_parts();
321
322 assert!(routing.resolve(&parts).is_err());
323 }
324}