lean_ctx/core/context_kernel/
identity_resolver.rs1use super::identity::{CallerIdentity, CallerRole};
4
5#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
7pub struct ResolverConfig {
8 pub user_header: String,
10 pub team_header: String,
12 pub cost_center_header: String,
14 pub role_header: String,
16 pub session_header: String,
18}
19
20impl Default for ResolverConfig {
21 fn default() -> Self {
22 Self {
23 user_header: "x-user-id".to_string(),
24 team_header: "x-team-id".to_string(),
25 cost_center_header: "x-cost-center".to_string(),
26 role_header: "x-caller-role".to_string(),
27 session_header: "x-session-id".to_string(),
28 }
29 }
30}
31
32pub fn resolve_from_headers(
34 config: &ResolverConfig,
35 headers: &[(String, String)],
36) -> CallerIdentity {
37 let mut identity = CallerIdentity::default();
38 enrich_identity(&mut identity, headers, config);
39 identity
40}
41
42pub fn parse_role(value: &str) -> CallerRole {
44 match value.trim().to_ascii_lowercase().as_str() {
45 "reviewer" => CallerRole::Reviewer,
46 "agent" => CallerRole::Agent,
47 "system" => CallerRole::System,
48 "admin" => CallerRole::Admin,
49 _ => CallerRole::Developer,
50 }
51}
52
53pub fn resolve_with_defaults(headers: &[(String, String)]) -> CallerIdentity {
55 resolve_from_headers(&ResolverConfig::default(), headers)
56}
57
58pub fn enrich_identity(
60 base: &mut CallerIdentity,
61 headers: &[(String, String)],
62 config: &ResolverConfig,
63) {
64 if base.user_id.is_none() {
65 base.user_id = header_value(headers, &config.user_header);
66 }
67 if base.team_id.is_none() {
68 base.team_id = header_value(headers, &config.team_header);
69 }
70 if base.cost_center.is_none() {
71 base.cost_center = header_value(headers, &config.cost_center_header);
72 }
73 if base.session_id.is_none() {
74 base.session_id = header_value(headers, &config.session_header);
75 }
76 if base.role == CallerRole::default()
77 && let Some(role) = header_value(headers, &config.role_header)
78 {
79 base.role = parse_role(&role);
80 }
81}
82
83fn header_value(headers: &[(String, String)], name: &str) -> Option<String> {
84 headers
85 .iter()
86 .find(|(header, _)| header.eq_ignore_ascii_case(name))
87 .map(|(_, value)| value)
88 .cloned()
89}
90
91#[cfg(test)]
92mod tests {
93 use super::{
94 CallerIdentity, CallerRole, ResolverConfig, enrich_identity, parse_role,
95 resolve_from_headers, resolve_with_defaults,
96 };
97
98 fn headers(values: &[(&str, &str)]) -> Vec<(String, String)> {
99 values
100 .iter()
101 .map(|(name, value)| ((*name).to_string(), (*value).to_string()))
102 .collect()
103 }
104
105 #[test]
106 fn resolve_empty_headers() {
107 assert_eq!(resolve_with_defaults(&[]), CallerIdentity::default());
108 }
109
110 #[test]
111 fn resolve_with_user_and_team() {
112 let identity = resolve_with_defaults(&headers(&[
113 ("x-user-id", "user-1"),
114 ("x-team-id", "team-1"),
115 ]));
116
117 assert_eq!(identity.user_id.as_deref(), Some("user-1"));
118 assert_eq!(identity.team_id.as_deref(), Some("team-1"));
119 }
120
121 #[test]
122 fn parse_role_case_insensitive() {
123 for value in ["Agent", "AGENT", "agent"] {
124 assert_eq!(parse_role(value), CallerRole::Agent);
125 }
126 }
127
128 #[test]
129 fn parse_role_unknown_defaults() {
130 assert_eq!(parse_role("unknown"), CallerRole::Developer);
131 }
132
133 #[test]
134 fn enrich_does_not_overwrite() {
135 let mut identity = CallerIdentity {
136 user_id: Some("existing".to_string()),
137 ..CallerIdentity::default()
138 };
139
140 enrich_identity(
141 &mut identity,
142 &headers(&[("x-user-id", "replacement"), ("x-team-id", "team-1")]),
143 &ResolverConfig::default(),
144 );
145
146 assert_eq!(identity.user_id.as_deref(), Some("existing"));
147 assert_eq!(identity.team_id.as_deref(), Some("team-1"));
148 }
149
150 #[test]
151 fn custom_header_names() {
152 let config = ResolverConfig {
153 user_header: "caller".to_string(),
154 team_header: "group".to_string(),
155 ..ResolverConfig::default()
156 };
157 let identity = resolve_from_headers(
158 &config,
159 &headers(&[("Caller", "user-2"), ("GROUP", "team-2")]),
160 );
161
162 assert_eq!(identity.user_id.as_deref(), Some("user-2"));
163 assert_eq!(identity.team_id.as_deref(), Some("team-2"));
164 }
165}