77
88from ..config import settings
99from ..database import AppSessionLocal
10+ from ..models .namespace_contact import NamespaceContact
1011from ..models .user import User , UserRole
1112
1213logger = logging .getLogger (__name__ )
@@ -28,6 +29,50 @@ def _parse_namespaces_header(raw: str) -> list[tuple[str, str]]:
2829 return pairs
2930
3031
32+ def _parse_namespace_emails_header (raw : str ) -> list [tuple [str , str , str ]]:
33+ """Parse 'ns1:cluster1=email@x.com,ns2:cluster2=email@y.com' into [(ns, cluster, email), ...]."""
34+ if not raw .strip ():
35+ return []
36+ result = []
37+ for entry in raw .split ("," ):
38+ entry = entry .strip ()
39+ if "=" not in entry :
40+ continue
41+ ns_cluster , email = entry .rsplit ("=" , 1 )
42+ email = email .strip ()
43+ if ":" not in ns_cluster or not email :
44+ continue
45+ ns , cluster = ns_cluster .split (":" , 1 )
46+ ns , cluster = ns .strip (), cluster .strip ()
47+ if ns and cluster :
48+ result .append ((ns , cluster , email ))
49+ return result
50+
51+
52+ async def _upsert_namespace_contacts (
53+ session : AsyncSession , contacts : list [tuple [str , str , str ]]
54+ ) -> None :
55+ """Upsert namespace escalation email contacts. Only writes if data changed."""
56+ if not contacts :
57+ return
58+ for ns , cluster , email in contacts :
59+ result = await session .execute (
60+ select (NamespaceContact ).where (
61+ NamespaceContact .namespace == ns ,
62+ NamespaceContact .cluster_name == cluster ,
63+ )
64+ )
65+ existing = result .scalar_one_or_none ()
66+ if existing :
67+ if existing .escalation_email != email :
68+ existing .escalation_email = email
69+ else :
70+ session .add (NamespaceContact (
71+ namespace = ns , cluster_name = cluster , escalation_email = email ,
72+ ))
73+ await session .commit ()
74+
75+
3176class CurrentUser :
3277 def __init__ (self , id : str , username : str , email : str , role : UserRole , namespaces : list [tuple [str , str ]]):
3378 self .id = id
@@ -94,6 +139,11 @@ def _to_current_user(user: User, namespaces: list[tuple[str, str]]) -> CurrentUs
94139async def _handle_dev_mode (session : AsyncSession ) -> CurrentUser :
95140 namespaces = _parse_namespaces_header (settings .dev_user_namespaces )
96141
142+ # Upsert dev namespace email contacts
143+ ns_emails = _parse_namespace_emails_header (settings .dev_namespace_emails )
144+ if ns_emails :
145+ await _upsert_namespace_contacts (session , ns_emails )
146+
97147 user_data = {
98148 "id" : settings .dev_user_id ,
99149 "username" : settings .dev_user_name ,
@@ -133,6 +183,12 @@ async def _handle_spoke_proxy(session: AsyncSession, request: Request) -> Curren
133183 user = await _get_or_create_user (session , user_data )
134184 user = await _sync_user_fields (session , user , user_data )
135185
186+ # Upsert namespace escalation email contacts from header
187+ ns_emails_raw = request .headers .get ("X-Forwarded-Namespace-Emails" , "" )
188+ ns_emails = _parse_namespace_emails_header (ns_emails_raw )
189+ if ns_emails :
190+ await _upsert_namespace_contacts (session , ns_emails )
191+
136192 logger .info ("Spoke proxy auth: user=%s, role=%s, namespaces=%d" , user_id , role .value , len (namespaces ))
137193 return _to_current_user (user , namespaces )
138194
0 commit comments