1use crate::config::SecurityConfig;
16use crate::types::{ClaimSource, InputAuthority, SecurityClaim};
17
18#[derive(Debug, Clone)]
20pub struct ChannelContext<'a> {
21 pub sender_id: &'a str,
23 pub chat_id: &'a str,
25 pub channel: &'a str,
27 pub sender_in_allowlist: bool,
29 pub allowlist_configured: bool,
31 pub threat_is_caution: bool,
33 pub trusted_sender_ids: &'a [String],
35}
36
37pub fn resolve_channel_claim(ctx: &ChannelContext<'_>, sec: &SecurityConfig) -> SecurityClaim {
42 let mut grants: Vec<InputAuthority> = Vec::new();
43 let mut sources: Vec<ClaimSource> = Vec::new();
44 let mut ceilings: Vec<InputAuthority> = Vec::new();
45
46 if ctx.allowlist_configured {
50 if ctx.sender_in_allowlist {
52 grants.push(sec.allowlist_authority);
53 sources.push(ClaimSource::ChannelAllowList);
54 }
55 }
57 if !ctx.trusted_sender_ids.is_empty() {
61 let is_trusted = ctx
62 .trusted_sender_ids
63 .iter()
64 .any(|id| id == ctx.chat_id || id == ctx.sender_id);
65 if is_trusted {
66 grants.push(sec.trusted_authority);
67 sources.push(ClaimSource::TrustedSenderId);
68 }
69 }
70
71 if ctx.threat_is_caution {
74 ceilings.push(sec.threat_caution_ceiling);
75 }
76
77 compose_claim(grants, sources, ceilings, ctx.sender_id, ctx.channel)
80}
81
82pub fn resolve_api_claim(
88 threat_is_caution: bool,
89 channel: &str,
90 sec: &SecurityConfig,
91) -> SecurityClaim {
92 let grants = vec![sec.api_authority];
93 let sources = vec![ClaimSource::ApiKey];
94 let mut ceilings: Vec<InputAuthority> = Vec::new();
95
96 if threat_is_caution {
97 ceilings.push(sec.threat_caution_ceiling);
98 }
99
100 compose_claim(grants, sources, ceilings, "api", channel)
101}
102
103pub fn resolve_a2a_claim(
108 threat_is_caution: bool,
109 sender_id: &str,
110 sec: &SecurityConfig,
111) -> SecurityClaim {
112 let grants = vec![InputAuthority::Peer];
113 let sources = vec![ClaimSource::A2aSession];
114 let mut ceilings: Vec<InputAuthority> = Vec::new();
115
116 if threat_is_caution {
117 ceilings.push(sec.threat_caution_ceiling);
118 }
119
120 compose_claim(grants, sources, ceilings, sender_id, "a2a")
121}
122
123fn compose_claim(
129 grants: Vec<InputAuthority>,
130 mut sources: Vec<ClaimSource>,
131 ceilings: Vec<InputAuthority>,
132 sender_id: &str,
133 channel: &str,
134) -> SecurityClaim {
135 let effective_grant = grants.iter().copied().max().unwrap_or_else(|| {
140 sources.push(ClaimSource::Anonymous);
141 InputAuthority::External
142 });
143
144 let effective_ceiling = ceilings
146 .iter()
147 .copied()
148 .min()
149 .unwrap_or(InputAuthority::Creator); let final_authority = effective_grant.min(effective_ceiling);
152 let threat_downgraded = !ceilings.is_empty() && final_authority < effective_grant;
153
154 SecurityClaim {
155 authority: final_authority,
156 sources,
157 ceiling: effective_ceiling,
158 threat_downgraded,
159 sender_id: sender_id.to_string(),
160 channel: channel.to_string(),
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 fn default_sec() -> SecurityConfig {
169 SecurityConfig::default()
170 }
171
172 fn channel_ctx<'a>(
173 sender_id: &'a str,
174 chat_id: &'a str,
175 channel: &'a str,
176 sender_in_allowlist: bool,
177 allowlist_configured: bool,
178 threat_is_caution: bool,
179 trusted: &'a [String],
180 ) -> ChannelContext<'a> {
181 ChannelContext {
182 sender_id,
183 chat_id,
184 channel,
185 sender_in_allowlist,
186 allowlist_configured,
187 threat_is_caution,
188 trusted_sender_ids: trusted,
189 }
190 }
191
192 #[test]
195 fn no_grants_yields_external() {
196 let sec = default_sec();
197 let ctx = channel_ctx("u1", "c1", "telegram", false, true, false, &[]);
198 let claim = resolve_channel_claim(&ctx, &sec);
199 assert_eq!(claim.authority, InputAuthority::External);
200 assert!(claim.sources.contains(&ClaimSource::Anonymous));
201 }
202
203 #[test]
204 fn allowlist_only_yields_peer() {
205 let sec = default_sec();
206 let ctx = channel_ctx("u1", "c1", "telegram", true, true, false, &[]);
207 let claim = resolve_channel_claim(&ctx, &sec);
208 assert_eq!(claim.authority, InputAuthority::Peer);
209 assert!(claim.sources.contains(&ClaimSource::ChannelAllowList));
210 }
211
212 #[test]
213 fn trusted_only_yields_creator() {
214 let sec = default_sec();
215 let trusted = vec!["u1".to_string()];
216 let ctx = channel_ctx("u1", "c1", "telegram", false, true, false, &trusted);
217 let claim = resolve_channel_claim(&ctx, &sec);
218 assert_eq!(claim.authority, InputAuthority::Creator);
219 assert!(claim.sources.contains(&ClaimSource::TrustedSenderId));
220 }
221
222 #[test]
223 fn trusted_by_chat_id() {
224 let sec = default_sec();
225 let trusted = vec!["c1".to_string()];
226 let ctx = channel_ctx("u1", "c1", "telegram", false, true, false, &trusted);
227 let claim = resolve_channel_claim(&ctx, &sec);
228 assert_eq!(claim.authority, InputAuthority::Creator);
229 }
230
231 #[test]
232 fn both_allowlist_and_trusted_yields_creator() {
233 let sec = default_sec();
235 let trusted = vec!["u1".to_string()];
236 let ctx = channel_ctx("u1", "c1", "telegram", true, true, false, &trusted);
237 let claim = resolve_channel_claim(&ctx, &sec);
238 assert_eq!(claim.authority, InputAuthority::Creator);
239 assert!(claim.sources.contains(&ClaimSource::ChannelAllowList));
240 assert!(claim.sources.contains(&ClaimSource::TrustedSenderId));
241 }
242
243 #[test]
246 fn threat_ceiling_downgrades_creator() {
247 let sec = default_sec();
248 let trusted = vec!["u1".to_string()];
249 let ctx = channel_ctx("u1", "c1", "telegram", true, true, true, &trusted);
250 let claim = resolve_channel_claim(&ctx, &sec);
251 assert_eq!(claim.authority, InputAuthority::External);
253 assert!(claim.threat_downgraded);
254 assert_eq!(claim.ceiling, InputAuthority::External);
255 }
256
257 #[test]
258 fn custom_threat_ceiling() {
259 let mut sec = default_sec();
260 sec.threat_caution_ceiling = InputAuthority::Peer;
261 let trusted = vec!["u1".to_string()];
262 let ctx = channel_ctx("u1", "c1", "telegram", true, true, true, &trusted);
263 let claim = resolve_channel_claim(&ctx, &sec);
264 assert_eq!(claim.authority, InputAuthority::Peer);
266 assert!(claim.threat_downgraded);
267 }
268
269 #[test]
270 fn no_threat_means_no_ceiling() {
271 let sec = default_sec();
272 let trusted = vec!["u1".to_string()];
273 let ctx = channel_ctx("u1", "c1", "telegram", true, true, false, &trusted);
274 let claim = resolve_channel_claim(&ctx, &sec);
275 assert_eq!(claim.authority, InputAuthority::Creator);
276 assert!(!claim.threat_downgraded);
277 assert_eq!(claim.ceiling, InputAuthority::Creator); }
279
280 #[test]
283 fn empty_allowlist_deny_on_empty_true_rejects() {
284 let sec = default_sec(); let ctx = channel_ctx("u1", "c1", "telegram", false, false, false, &[]);
286 let claim = resolve_channel_claim(&ctx, &sec);
287 assert_eq!(claim.authority, InputAuthority::External);
288 assert!(claim.sources.contains(&ClaimSource::Anonymous));
289 }
290
291 #[test]
292 fn empty_allowlist_still_rejects_even_if_flag_is_false() {
293 let mut sec = default_sec();
294 sec.deny_on_empty_allowlist = false;
297 let ctx = channel_ctx("u1", "c1", "telegram", false, false, false, &[]);
298 let claim = resolve_channel_claim(&ctx, &sec);
299 assert_eq!(claim.authority, InputAuthority::External);
300 assert!(claim.sources.contains(&ClaimSource::Anonymous));
301 }
302
303 #[test]
306 fn api_claim_default_creator() {
307 let sec = default_sec();
308 let claim = resolve_api_claim(false, "api", &sec);
309 assert_eq!(claim.authority, InputAuthority::Creator);
310 assert!(claim.sources.contains(&ClaimSource::ApiKey));
311 }
312
313 #[test]
314 fn api_claim_threat_downgrade() {
315 let sec = default_sec();
316 let claim = resolve_api_claim(true, "api", &sec);
317 assert_eq!(claim.authority, InputAuthority::External);
318 assert!(claim.threat_downgraded);
319 }
320
321 #[test]
324 fn a2a_claim_always_peer() {
325 let sec = default_sec();
326 let claim = resolve_a2a_claim(false, "peer-agent", &sec);
327 assert_eq!(claim.authority, InputAuthority::Peer);
328 assert!(claim.sources.contains(&ClaimSource::A2aSession));
329 }
330
331 #[test]
332 fn a2a_claim_threat_downgrade() {
333 let sec = default_sec();
334 let claim = resolve_a2a_claim(true, "peer-agent", &sec);
335 assert_eq!(claim.authority, InputAuthority::External);
336 assert!(claim.threat_downgraded);
337 }
338
339 #[test]
342 fn custom_allowlist_authority() {
343 let mut sec = default_sec();
344 sec.allowlist_authority = InputAuthority::Creator;
345 let ctx = channel_ctx("u1", "c1", "telegram", true, true, false, &[]);
346 let claim = resolve_channel_claim(&ctx, &sec);
347 assert_eq!(claim.authority, InputAuthority::Creator);
348 }
349
350 #[test]
351 fn custom_api_authority_downgraded() {
352 let mut sec = default_sec();
353 sec.api_authority = InputAuthority::Peer;
354 let claim = resolve_api_claim(false, "api", &sec);
355 assert_eq!(claim.authority, InputAuthority::Peer);
356 }
357
358 #[test]
361 fn adding_grant_never_decreases_authority() {
362 let sec = default_sec();
363 let ctx1 = channel_ctx("u1", "c1", "telegram", true, true, false, &[]);
365 let claim1 = resolve_channel_claim(&ctx1, &sec);
366
367 let trusted = vec!["u1".to_string()];
369 let ctx2 = channel_ctx("u1", "c1", "telegram", true, true, false, &trusted);
370 let claim2 = resolve_channel_claim(&ctx2, &sec);
371
372 assert!(claim2.authority >= claim1.authority);
373 }
374
375 #[test]
376 fn adding_ceiling_never_increases_authority() {
377 let sec = default_sec();
378 let trusted = vec!["u1".to_string()];
379
380 let ctx1 = channel_ctx("u1", "c1", "telegram", true, true, false, &trusted);
382 let claim1 = resolve_channel_claim(&ctx1, &sec);
383
384 let ctx2 = channel_ctx("u1", "c1", "telegram", true, true, true, &trusted);
386 let claim2 = resolve_channel_claim(&ctx2, &sec);
387
388 assert!(claim2.authority <= claim1.authority);
389 }
390
391 #[test]
394 fn threat_present_but_not_binding_does_not_set_downgraded() {
395 let sec = default_sec();
398 let ctx = channel_ctx("unknown", "c1", "telegram", false, true, true, &[]);
399 let claim = resolve_channel_claim(&ctx, &sec);
400 assert_eq!(claim.authority, InputAuthority::External);
401 assert!(!claim.threat_downgraded);
403 }
404
405 #[test]
406 fn api_claim_with_custom_ceiling_and_threat() {
407 let mut sec = default_sec();
409 sec.api_authority = InputAuthority::Peer;
410 sec.threat_caution_ceiling = InputAuthority::Peer;
411 let claim = resolve_api_claim(true, "api", &sec);
412 assert_eq!(claim.authority, InputAuthority::Peer);
413 assert!(!claim.threat_downgraded); let mut sec2 = default_sec();
417 sec2.threat_caution_ceiling = InputAuthority::Peer;
418 let claim2 = resolve_api_claim(true, "api", &sec2);
419 assert_eq!(claim2.authority, InputAuthority::Peer);
420 assert!(claim2.threat_downgraded); }
422}