Skip to main content

locus_gateway/
tenant.rs

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}