1use async_trait::async_trait;
6use modkit_security::SecurityContext;
7use tenant_resolver_sdk::{
8 AccessOptions, TenantFilter, TenantId, TenantInfo, TenantResolverError,
9 TenantResolverPluginClient,
10};
11
12use super::service::Service;
13
14#[async_trait]
15impl TenantResolverPluginClient for Service {
16 async fn get_tenant(
17 &self,
18 _ctx: &SecurityContext,
19 id: TenantId,
20 ) -> Result<TenantInfo, TenantResolverError> {
21 self.tenants
22 .get(&id)
23 .cloned()
24 .ok_or(TenantResolverError::TenantNotFound { tenant_id: id })
25 }
26
27 async fn can_access(
28 &self,
29 ctx: &SecurityContext,
30 target: TenantId,
31 _options: Option<&AccessOptions>,
32 ) -> Result<bool, TenantResolverError> {
33 let source = ctx.tenant_id();
34
35 if !self.tenants.contains_key(&target) {
37 return Err(TenantResolverError::TenantNotFound { tenant_id: target });
38 }
39
40 if source == target {
42 return Ok(true);
43 }
44
45 Ok(self.access_rules.contains(&(source, target)))
47 }
48
49 async fn get_accessible_tenants(
50 &self,
51 ctx: &SecurityContext,
52 filter: Option<&TenantFilter>,
53 _options: Option<&AccessOptions>,
54 ) -> Result<Vec<TenantInfo>, TenantResolverError> {
55 let source = ctx.tenant_id();
56
57 let mut items: Vec<TenantInfo> = Vec::new();
58
59 if let Some(self_info) = self.tenants.get(&source)
61 && Self::matches_filter(self_info, filter)
62 {
63 items.push(self_info.clone());
64 }
65
66 let accessible_ids = self.accessible_by.get(&source);
68
69 if let Some(ids) = accessible_ids {
71 for id in ids {
72 if *id == source {
74 continue;
75 }
76 if let Some(info) = self.tenants.get(id)
77 && Self::matches_filter(info, filter)
78 {
79 items.push(info.clone());
80 }
81 }
82 }
83
84 Ok(items)
85 }
86}
87
88#[cfg(test)]
89#[cfg_attr(coverage_nightly, coverage(off))]
90mod tests {
91 use super::*;
92 use crate::config::{AccessRuleConfig, StaticTrPluginConfig, TenantConfig};
93 use tenant_resolver_sdk::TenantStatus;
94 use uuid::Uuid;
95
96 fn tenant(id: &str, name: &str, status: TenantStatus) -> TenantConfig {
98 TenantConfig {
99 id: Uuid::parse_str(id).unwrap(),
100 name: name.to_owned(),
101 status,
102 tenant_type: None,
103 }
104 }
105
106 fn access_rule(source: &str, target: &str) -> AccessRuleConfig {
108 AccessRuleConfig {
109 source: Uuid::parse_str(source).unwrap(),
110 target: Uuid::parse_str(target).unwrap(),
111 }
112 }
113
114 fn ctx_for_tenant(tenant_id: &str) -> SecurityContext {
116 SecurityContext::builder()
117 .tenant_id(Uuid::parse_str(tenant_id).unwrap())
118 .build()
119 }
120
121 fn active_filter() -> TenantFilter {
123 TenantFilter {
124 status: vec![TenantStatus::Active],
125 ..Default::default()
126 }
127 }
128
129 const TENANT_A: &str = "11111111-1111-1111-1111-111111111111";
131 const TENANT_B: &str = "22222222-2222-2222-2222-222222222222";
132 const TENANT_C: &str = "33333333-3333-3333-3333-333333333333";
133 const NONEXISTENT: &str = "99999999-9999-9999-9999-999999999999";
134
135 #[tokio::test]
138 async fn get_tenant_existing() {
139 let cfg = StaticTrPluginConfig {
140 tenants: vec![tenant(TENANT_A, "Tenant A", TenantStatus::Active)],
141 ..Default::default()
142 };
143 let service = Service::from_config(&cfg);
144 let ctx = ctx_for_tenant(TENANT_A);
145
146 let result = service
147 .get_tenant(&ctx, Uuid::parse_str(TENANT_A).unwrap())
148 .await;
149
150 assert!(result.is_ok());
151 let info = result.unwrap();
152 assert_eq!(info.name, "Tenant A");
153 assert_eq!(info.status, TenantStatus::Active);
154 }
155
156 #[tokio::test]
157 async fn get_tenant_nonexistent() {
158 let cfg = StaticTrPluginConfig {
159 tenants: vec![tenant(TENANT_A, "Tenant A", TenantStatus::Active)],
160 ..Default::default()
161 };
162 let service = Service::from_config(&cfg);
163 let ctx = ctx_for_tenant(TENANT_A);
164 let nonexistent_id = Uuid::parse_str(NONEXISTENT).unwrap();
165
166 let result = service.get_tenant(&ctx, nonexistent_id).await;
167
168 assert!(result.is_err());
169 match result.unwrap_err() {
170 TenantResolverError::TenantNotFound { tenant_id } => {
171 assert_eq!(tenant_id, nonexistent_id);
172 }
173 other => panic!("Expected TenantNotFound, got: {other:?}"),
174 }
175 }
176
177 #[tokio::test]
178 async fn get_tenant_empty_service() {
179 let cfg = StaticTrPluginConfig::default();
180 let service = Service::from_config(&cfg);
181 let ctx = ctx_for_tenant(TENANT_A);
182 let tenant_a_id = Uuid::parse_str(TENANT_A).unwrap();
183
184 let result = service.get_tenant(&ctx, tenant_a_id).await;
185
186 assert!(result.is_err());
187 match result.unwrap_err() {
188 TenantResolverError::TenantNotFound { tenant_id } => {
189 assert_eq!(tenant_id, tenant_a_id);
190 }
191 other => panic!("Expected TenantNotFound, got: {other:?}"),
192 }
193 }
194
195 #[tokio::test]
196 async fn get_tenant_returns_any_status() {
197 let cfg = StaticTrPluginConfig {
199 tenants: vec![tenant(TENANT_A, "Tenant A", TenantStatus::Suspended)],
200 ..Default::default()
201 };
202 let service = Service::from_config(&cfg);
203 let ctx = ctx_for_tenant(TENANT_A);
204 let tenant_a_id = Uuid::parse_str(TENANT_A).unwrap();
205
206 let result = service.get_tenant(&ctx, tenant_a_id).await;
208 assert!(result.is_ok());
209 assert_eq!(result.unwrap().status, TenantStatus::Suspended);
210 }
211
212 #[tokio::test]
215 async fn can_access_allowed() {
216 let cfg = StaticTrPluginConfig {
217 tenants: vec![
218 tenant(TENANT_A, "A", TenantStatus::Active),
219 tenant(TENANT_B, "B", TenantStatus::Active),
220 ],
221 access_rules: vec![access_rule(TENANT_A, TENANT_B)],
222 ..Default::default()
223 };
224 let service = Service::from_config(&cfg);
225 let ctx = ctx_for_tenant(TENANT_A);
226
227 let result = service
228 .can_access(&ctx, Uuid::parse_str(TENANT_B).unwrap(), None)
229 .await;
230
231 assert!(result.is_ok());
232 assert!(result.unwrap());
233 }
234
235 #[tokio::test]
236 async fn can_access_denied_no_rule() {
237 let cfg = StaticTrPluginConfig {
238 tenants: vec![
239 tenant(TENANT_A, "A", TenantStatus::Active),
240 tenant(TENANT_B, "B", TenantStatus::Active),
241 ],
242 access_rules: vec![], ..Default::default()
244 };
245 let service = Service::from_config(&cfg);
246 let ctx = ctx_for_tenant(TENANT_A);
247
248 let result = service
249 .can_access(&ctx, Uuid::parse_str(TENANT_B).unwrap(), None)
250 .await;
251
252 assert!(result.is_ok());
253 assert!(!result.unwrap());
254 }
255
256 #[tokio::test]
257 async fn can_access_error_for_nonexistent_target() {
258 let cfg = StaticTrPluginConfig {
259 tenants: vec![tenant(TENANT_A, "A", TenantStatus::Active)],
260 access_rules: vec![],
261 ..Default::default()
262 };
263 let service = Service::from_config(&cfg);
264 let ctx = ctx_for_tenant(TENANT_A);
265 let nonexistent_id = Uuid::parse_str(NONEXISTENT).unwrap();
266
267 let result = service.can_access(&ctx, nonexistent_id, None).await;
268
269 assert!(result.is_err());
270 match result.unwrap_err() {
271 TenantResolverError::TenantNotFound { tenant_id } => {
272 assert_eq!(tenant_id, nonexistent_id);
273 }
274 other => panic!("Expected TenantNotFound, got: {other:?}"),
275 }
276 }
277
278 #[tokio::test]
279 async fn can_access_not_symmetric() {
280 let cfg = StaticTrPluginConfig {
282 tenants: vec![
283 tenant(TENANT_A, "A", TenantStatus::Active),
284 tenant(TENANT_B, "B", TenantStatus::Active),
285 ],
286 access_rules: vec![access_rule(TENANT_A, TENANT_B)], ..Default::default()
288 };
289 let service = Service::from_config(&cfg);
290
291 let ctx_a = ctx_for_tenant(TENANT_A);
293 let result = service
294 .can_access(&ctx_a, Uuid::parse_str(TENANT_B).unwrap(), None)
295 .await;
296 assert!(result.unwrap());
297
298 let ctx_b = ctx_for_tenant(TENANT_B);
300 let result = service
301 .can_access(&ctx_b, Uuid::parse_str(TENANT_A).unwrap(), None)
302 .await;
303 assert!(!result.unwrap());
304 }
305
306 #[tokio::test]
307 async fn can_access_not_transitive() {
308 let cfg = StaticTrPluginConfig {
310 tenants: vec![
311 tenant(TENANT_A, "A", TenantStatus::Active),
312 tenant(TENANT_B, "B", TenantStatus::Active),
313 tenant(TENANT_C, "C", TenantStatus::Active),
314 ],
315 access_rules: vec![
316 access_rule(TENANT_A, TENANT_B), access_rule(TENANT_B, TENANT_C), ],
319 ..Default::default()
320 };
321 let service = Service::from_config(&cfg);
322 let ctx = ctx_for_tenant(TENANT_A);
323
324 let result = service
326 .can_access(&ctx, Uuid::parse_str(TENANT_B).unwrap(), None)
327 .await;
328 assert!(result.unwrap());
329
330 let result = service
332 .can_access(&ctx, Uuid::parse_str(TENANT_C).unwrap(), None)
333 .await;
334 assert!(!result.unwrap());
335 }
336
337 #[tokio::test]
338 async fn can_access_self_allowed() {
339 let cfg = StaticTrPluginConfig {
341 tenants: vec![tenant(TENANT_A, "A", TenantStatus::Active)],
342 access_rules: vec![], ..Default::default()
344 };
345 let service = Service::from_config(&cfg);
346 let ctx = ctx_for_tenant(TENANT_A);
347
348 let result = service
350 .can_access(&ctx, Uuid::parse_str(TENANT_A).unwrap(), None)
351 .await;
352 assert!(result.unwrap());
353 }
354
355 #[tokio::test]
356 async fn can_access_allows_any_status() {
357 let cfg = StaticTrPluginConfig {
359 tenants: vec![
360 tenant(TENANT_A, "A", TenantStatus::Active),
361 tenant(TENANT_B, "B", TenantStatus::Suspended),
362 ],
363 access_rules: vec![access_rule(TENANT_A, TENANT_B)],
364 ..Default::default()
365 };
366 let service = Service::from_config(&cfg);
367 let ctx = ctx_for_tenant(TENANT_A);
368 let tenant_b_id = Uuid::parse_str(TENANT_B).unwrap();
369
370 let result = service.can_access(&ctx, tenant_b_id, None).await;
372 assert!(result.unwrap());
373 }
374
375 #[tokio::test]
378 async fn get_accessible_tenants_with_rules() {
379 let cfg = StaticTrPluginConfig {
380 tenants: vec![
381 tenant(TENANT_A, "A", TenantStatus::Active),
382 tenant(TENANT_B, "B", TenantStatus::Active),
383 tenant(TENANT_C, "C", TenantStatus::Active),
384 ],
385 access_rules: vec![
386 access_rule(TENANT_A, TENANT_B),
387 access_rule(TENANT_A, TENANT_C),
388 ],
389 ..Default::default()
390 };
391 let service = Service::from_config(&cfg);
392 let ctx = ctx_for_tenant(TENANT_A);
393
394 let result = service.get_accessible_tenants(&ctx, None, None).await;
395
396 assert!(result.is_ok());
397 let items = result.unwrap();
398 assert_eq!(items.len(), 3);
400
401 assert_eq!(items[0].id, Uuid::parse_str(TENANT_A).unwrap());
403
404 let ids: Vec<_> = items.iter().map(|t| t.id).collect();
405 assert!(ids.contains(&Uuid::parse_str(TENANT_B).unwrap()));
406 assert!(ids.contains(&Uuid::parse_str(TENANT_C).unwrap()));
407 }
408
409 #[tokio::test]
410 async fn get_accessible_tenants_no_rules() {
411 let cfg = StaticTrPluginConfig {
412 tenants: vec![
413 tenant(TENANT_A, "A", TenantStatus::Active),
414 tenant(TENANT_B, "B", TenantStatus::Active),
415 ],
416 access_rules: vec![],
417 ..Default::default()
418 };
419 let service = Service::from_config(&cfg);
420 let ctx = ctx_for_tenant(TENANT_A);
421
422 let result = service.get_accessible_tenants(&ctx, None, None).await;
423
424 assert!(result.is_ok());
425 let items = result.unwrap();
426 assert_eq!(items.len(), 1);
428 assert_eq!(items[0].id, Uuid::parse_str(TENANT_A).unwrap());
429 }
430
431 #[tokio::test]
432 async fn get_accessible_tenants_missing_tenant_info() {
433 let cfg = StaticTrPluginConfig {
435 tenants: vec![tenant(TENANT_A, "A", TenantStatus::Active)],
436 access_rules: vec![access_rule(TENANT_A, TENANT_B)], ..Default::default()
438 };
439 let service = Service::from_config(&cfg);
440 let ctx = ctx_for_tenant(TENANT_A);
441
442 let result = service.get_accessible_tenants(&ctx, None, None).await;
443
444 assert!(result.is_ok());
445 let items = result.unwrap();
446 assert_eq!(items.len(), 1);
449 assert_eq!(items[0].id, Uuid::parse_str(TENANT_A).unwrap());
450 }
451
452 #[tokio::test]
453 async fn get_accessible_tenants_filtered_by_status() {
454 let cfg = StaticTrPluginConfig {
455 tenants: vec![
456 tenant(TENANT_A, "A", TenantStatus::Active),
457 tenant(TENANT_B, "B", TenantStatus::Active),
458 tenant(TENANT_C, "C", TenantStatus::Suspended),
459 ],
460 access_rules: vec![
461 access_rule(TENANT_A, TENANT_B),
462 access_rule(TENANT_A, TENANT_C),
463 ],
464 ..Default::default()
465 };
466 let service = Service::from_config(&cfg);
467 let ctx = ctx_for_tenant(TENANT_A);
468
469 let result = service.get_accessible_tenants(&ctx, None, None).await;
471 assert_eq!(result.unwrap().len(), 3);
472
473 let filter = active_filter();
475 let result = service
476 .get_accessible_tenants(&ctx, Some(&filter), None)
477 .await;
478 let items = result.unwrap();
479 assert_eq!(items.len(), 2);
480 assert_eq!(items[0].id, Uuid::parse_str(TENANT_A).unwrap());
482 assert_eq!(items[1].id, Uuid::parse_str(TENANT_B).unwrap());
483 }
484
485 #[tokio::test]
486 async fn get_accessible_tenants_filtered_by_id() {
487 let cfg = StaticTrPluginConfig {
488 tenants: vec![
489 tenant(TENANT_A, "A", TenantStatus::Active),
490 tenant(TENANT_B, "B", TenantStatus::Active),
491 tenant(TENANT_C, "C", TenantStatus::Active),
492 ],
493 access_rules: vec![
494 access_rule(TENANT_A, TENANT_B),
495 access_rule(TENANT_A, TENANT_C),
496 ],
497 ..Default::default()
498 };
499 let service = Service::from_config(&cfg);
500 let ctx = ctx_for_tenant(TENANT_A);
501
502 let filter = TenantFilter {
504 id: vec![Uuid::parse_str(TENANT_B).unwrap()],
505 ..Default::default()
506 };
507 let result = service
508 .get_accessible_tenants(&ctx, Some(&filter), None)
509 .await;
510 let items = result.unwrap();
511 assert_eq!(items.len(), 1);
512 assert_eq!(items[0].id, Uuid::parse_str(TENANT_B).unwrap());
513 }
514}