1use axum::http::HeaderMap;
2
3use locus_core_rs::domain::models as core_models;
4
5use crate::constants::{
6 DEFAULT_TENANT, TENANT_HEADER, TENANT_HEADERS, TENANT_SCOPE_PREFIX, TENANT_SCOPE_SEPARATOR,
7};
8
9pub(crate) fn normalize_tenant_value(value: &str) -> Option<String> {
10 let trimmed = value.trim();
11 if trimmed.is_empty() {
12 return None;
13 }
14
15 let normalized = trimmed.to_ascii_lowercase();
16 if normalized
17 .chars()
18 .all(|ch| ch.is_ascii_alphanumeric() || ch == '-' || ch == '_')
19 {
20 Some(normalized)
21 } else {
22 None
23 }
24}
25
26pub(crate) fn resolve_http_tenant(explicit_tenant: Option<&str>, headers: &HeaderMap) -> String {
27 explicit_tenant
28 .and_then(normalize_tenant_value)
29 .or_else(|| resolve_tenant_header(headers))
30 .unwrap_or_else(|| DEFAULT_TENANT.to_string())
31}
32
33pub(crate) fn resolve_grpc_tenant(metadata: &tonic::metadata::MetadataMap) -> String {
34 metadata
35 .get(TENANT_HEADER)
36 .and_then(|value| value.to_str().ok())
37 .and_then(normalize_tenant_value)
38 .unwrap_or_else(|| DEFAULT_TENANT.to_string())
39}
40
41pub(crate) fn resolve_tenant_header(headers: &HeaderMap) -> Option<String> {
42 TENANT_HEADERS.iter().find_map(|name| {
43 headers
44 .get(*name)
45 .and_then(|value| value.to_str().ok())
46 .and_then(normalize_tenant_value)
47 })
48}
49
50pub(crate) fn parse_scoped_session_id(session_id: &str) -> Option<(&str, &str)> {
51 let remainder = session_id.strip_prefix(TENANT_SCOPE_PREFIX)?;
52 remainder.split_once(TENANT_SCOPE_SEPARATOR)
53}
54
55pub(crate) fn is_default_tenant(tenant: &str) -> bool {
56 tenant == DEFAULT_TENANT
57}
58
59pub(crate) fn scope_session_id(tenant: &str, session_id: &str) -> String {
60 if is_default_tenant(tenant) {
61 session_id.to_string()
62 } else {
63 format!("{TENANT_SCOPE_PREFIX}{tenant}{TENANT_SCOPE_SEPARATOR}{session_id}")
64 }
65}
66
67pub(crate) fn session_belongs_to_tenant(session_id: &str, tenant: &str) -> bool {
68 match parse_scoped_session_id(session_id) {
69 Some((scoped_tenant, _)) => scoped_tenant == tenant,
70 None => is_default_tenant(tenant),
71 }
72}
73
74pub(crate) fn display_session_id(session_id: &str) -> String {
75 match parse_scoped_session_id(session_id) {
76 Some((_, base_session_id)) => base_session_id.to_string(),
77 None => session_id.to_string(),
78 }
79}
80
81pub(crate) fn normalize_node_for_tenant(
82 mut node: core_models::SttpNode,
83 tenant: &str,
84) -> Option<core_models::SttpNode> {
85 if !session_belongs_to_tenant(&node.session_id, tenant) {
86 return None;
87 }
88
89 node.session_id = display_session_id(&node.session_id);
90 Some(node)
91}