1use std::collections::HashMap;
7
8use crate::{
9 AccessDecision, Action, AuditFilter, AuditLog, AuditRecord, AuthzError, AuthzResult,
10 RateLimitDecision, RateLimitRule, RateLimiter, RedactionConfig, Resource, Role,
11};
12
13const DEFAULT_AUDIT_MAX_SIZE: usize = 1000;
15
16#[derive(Debug)]
38pub struct AuthzEngine {
39 roles: HashMap<String, Role>,
40 actor_roles: HashMap<String, String>,
41 rate_limiter: RateLimiter,
42 audit_log: AuditLog,
43 redaction_config: RedactionConfig,
44 approval_required: Vec<ApprovalRule>,
45}
46
47#[derive(Debug, Clone)]
49struct ApprovalRule {
50 resource_type: Option<String>,
51 action: Option<Action>,
52}
53
54impl AuthzEngine {
55 pub fn authorize(
76 &mut self,
77 actor_id: &str,
78 resource: &Resource,
79 action: &Action,
80 ) -> AccessDecision {
81 let role = if let Some(r) = self.resolve_role(actor_id) {
83 r
84 } else {
85 let decision =
86 AccessDecision::denied(format!("actor '{actor_id}' has no assigned role"));
87 self.record_audit(actor_id, action, resource, &decision);
88 return decision;
89 };
90
91 let decision = role.check(resource.resource_type(), action);
93 if decision.is_denied() {
94 self.record_audit(actor_id, action, resource, &decision);
95 return decision;
96 }
97
98 let rate_decision = self.rate_limiter.check_and_record(actor_id, resource.resource_type());
100 if let RateLimitDecision::Exceeded { retry_after } = rate_decision {
101 let decision = AccessDecision::denied(format!(
102 "rate limit exceeded for '{}' on '{}' (retry after {}ms)",
103 actor_id,
104 resource.resource_type(),
105 retry_after.as_millis(),
106 ));
107 self.record_audit(actor_id, action, resource, &decision);
108 return decision;
109 }
110
111 for rule in &self.approval_required {
113 let resource_match =
114 rule.resource_type.as_ref().is_none_or(|rt| rt == resource.resource_type());
115 let action_match = rule.action.as_ref().is_none_or(|a| a == action);
116
117 if resource_match && action_match {
118 let decision = AccessDecision::requires_approval(format!(
119 "action '{action}' on '{}' requires explicit approval",
120 resource.resource_type(),
121 ));
122 self.record_audit(actor_id, action, resource, &decision);
123 return decision;
124 }
125 }
126
127 let decision = AccessDecision::Allowed;
129 self.record_audit(actor_id, action, resource, &decision);
130 decision
131 }
132
133 pub fn add_role(&mut self, role: Role) {
137 self.roles.insert(role.name().to_owned(), role);
138 }
139
140 pub fn assign_role(&mut self, actor_id: &str, role_name: &str) -> AuthzResult<()> {
144 if !self.roles.contains_key(role_name) {
145 return Err(AuthzError::invalid_role(role_name));
146 }
147 self.actor_roles.insert(actor_id.to_owned(), role_name.to_owned());
148 Ok(())
149 }
150
151 pub fn remove_role(&mut self, actor_id: &str) {
153 self.actor_roles.remove(actor_id);
154 }
155
156 #[must_use]
158 pub fn actor_role(&self, actor_id: &str) -> Option<&str> {
159 self.actor_roles.get(actor_id).map(String::as_str)
160 }
161
162 #[must_use]
164 pub const fn rate_limiter(&self) -> &RateLimiter {
165 &self.rate_limiter
166 }
167
168 pub const fn rate_limiter_mut(&mut self) -> &mut RateLimiter {
170 &mut self.rate_limiter
171 }
172
173 #[must_use]
175 pub const fn audit_log(&self) -> &AuditLog {
176 &self.audit_log
177 }
178
179 #[must_use]
181 pub fn query_audit(&self, filter: &AuditFilter) -> Vec<&AuditRecord> {
182 self.audit_log.query(filter)
183 }
184
185 #[must_use]
187 pub const fn redaction_config(&self) -> &RedactionConfig {
188 &self.redaction_config
189 }
190
191 pub fn redact(&self, value: &mut serde_json::Value) {
193 crate::redact_value(value, &self.redaction_config);
194 }
195
196 pub fn add_rate_limit_rule(&mut self, rule: RateLimitRule) {
198 self.rate_limiter.add_rule(rule);
199 }
200
201 pub fn require_approval(&mut self, resource_type: Option<String>, action: Option<Action>) {
203 self.approval_required.push(ApprovalRule { resource_type, action });
204 }
205
206 fn resolve_role(&self, actor_id: &str) -> Option<Role> {
209 let role_name = self.actor_roles.get(actor_id)?;
210 self.roles.get(role_name).cloned()
211 }
212
213 fn record_audit(
214 &mut self,
215 actor_id: &str,
216 action: &Action,
217 resource: &Resource,
218 decision: &AccessDecision,
219 ) {
220 let record = AuditRecord::new(actor_id, *action, resource.clone(), decision.clone());
221 self.audit_log.record(record);
222 }
223}
224
225#[derive(Debug)]
242pub struct AuthzEngineBuilder {
243 roles: HashMap<String, Role>,
244 actor_roles: HashMap<String, String>,
245 rate_limit_rules: Vec<RateLimitRule>,
246 redaction_config: RedactionConfig,
247 audit_max_size: usize,
248 approval_rules: Vec<ApprovalRule>,
249}
250
251impl AuthzEngineBuilder {
252 #[must_use]
254 pub fn new() -> Self {
255 Self {
256 roles: HashMap::new(),
257 actor_roles: HashMap::new(),
258 rate_limit_rules: Vec::new(),
259 redaction_config: RedactionConfig::default(),
260 audit_max_size: DEFAULT_AUDIT_MAX_SIZE,
261 approval_rules: Vec::new(),
262 }
263 }
264
265 #[must_use]
267 pub fn add_role(mut self, role: Role) -> Self {
268 self.roles.insert(role.name().to_owned(), role);
269 self
270 }
271
272 #[must_use]
277 pub fn assign_role(
278 mut self,
279 actor_id: impl Into<String>,
280 role_name: impl Into<String>,
281 ) -> Self {
282 self.actor_roles.insert(actor_id.into(), role_name.into());
283 self
284 }
285
286 #[must_use]
288 pub fn rate_limit_rule(mut self, rule: RateLimitRule) -> Self {
289 self.rate_limit_rules.push(rule);
290 self
291 }
292
293 #[must_use]
295 pub fn redaction_config(mut self, config: RedactionConfig) -> Self {
296 self.redaction_config = config;
297 self
298 }
299
300 #[must_use]
302 pub const fn audit_max_size(mut self, max_size: usize) -> Self {
303 self.audit_max_size = max_size;
304 self
305 }
306
307 #[must_use]
309 pub fn require_approval(
310 mut self,
311 resource_type: Option<String>,
312 action: Option<Action>,
313 ) -> Self {
314 self.approval_rules.push(ApprovalRule { resource_type, action });
315 self
316 }
317
318 pub fn build_checked(self) -> AuthzResult<AuthzEngine> {
321 self.validate_assignments()?;
322 Ok(self.build_unchecked())
323 }
324
325 #[must_use]
333 pub fn build(self) -> AuthzEngine {
334 self.build_checked().expect("invalid AuthzEngineBuilder configuration")
335 }
336
337 fn validate_assignments(&self) -> AuthzResult<()> {
338 if let Some(role_name) =
339 self.actor_roles.values().find(|role_name| !self.roles.contains_key(role_name.as_str()))
340 {
341 return Err(AuthzError::invalid_role(role_name.clone()));
342 }
343 Ok(())
344 }
345
346 fn build_unchecked(self) -> AuthzEngine {
347 let mut rate_limiter = RateLimiter::new();
348 for rule in self.rate_limit_rules {
349 rate_limiter.add_rule(rule);
350 }
351
352 AuthzEngine {
353 roles: self.roles,
354 actor_roles: self.actor_roles,
355 rate_limiter,
356 audit_log: AuditLog::new(self.audit_max_size),
357 redaction_config: self.redaction_config,
358 approval_required: self.approval_rules,
359 }
360 }
361}
362
363impl Default for AuthzEngineBuilder {
364 fn default() -> Self {
365 Self::new()
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use crate::{PermissionLevel, RoleBuilder};
372 use std::time::Duration;
373
374 use super::*;
375
376 fn basic_engine() -> AuthzEngine {
377 AuthzEngineBuilder::new()
378 .add_role(Role::admin())
379 .add_role(Role::viewer())
380 .add_role(Role::none())
381 .assign_role("alice", "admin")
382 .assign_role("bob", "viewer")
383 .assign_role("nobody", "none")
384 .build()
385 }
386
387 #[test]
390 fn admin_can_do_everything() {
391 let mut engine = basic_engine();
392 for &action in Action::all() {
393 let d = engine.authorize("alice", &Resource::new("orders"), &action);
394 assert!(d.is_allowed(), "admin should allow {action}");
395 }
396 }
397
398 #[test]
399 fn viewer_can_only_read() {
400 let mut engine = basic_engine();
401 assert!(engine.authorize("bob", &Resource::new("orders"), &Action::Read).is_allowed());
402 assert!(engine.authorize("bob", &Resource::new("orders"), &Action::Create).is_denied());
403 }
404
405 #[test]
406 fn none_role_denied() {
407 let mut engine = basic_engine();
408 assert!(engine.authorize("nobody", &Resource::new("orders"), &Action::Read).is_denied());
409 }
410
411 #[test]
412 fn unknown_actor_denied() {
413 let mut engine = basic_engine();
414 let d = engine.authorize("ghost", &Resource::new("orders"), &Action::Read);
415 assert!(d.is_denied());
416 assert!(d.reason().unwrap().contains("no assigned role"));
417 }
418
419 #[test]
422 fn rate_limit_blocks_after_max() {
423 let mut engine = AuthzEngineBuilder::new()
424 .add_role(Role::admin())
425 .assign_role("alice", "admin")
426 .rate_limit_rule(RateLimitRule::new("orders", 2, Duration::from_secs(60)))
427 .build();
428
429 assert!(engine.authorize("alice", &Resource::new("orders"), &Action::Read).is_allowed());
430 assert!(engine.authorize("alice", &Resource::new("orders"), &Action::Read).is_allowed());
431 let d = engine.authorize("alice", &Resource::new("orders"), &Action::Read);
432 assert!(d.is_denied());
433 assert!(d.reason().unwrap().contains("rate limit"));
434 }
435
436 #[test]
439 fn approval_required_triggers() {
440 let mut engine = AuthzEngineBuilder::new()
441 .add_role(Role::admin())
442 .assign_role("alice", "admin")
443 .require_approval(Some("orders".to_owned()), Some(Action::Delete))
444 .build();
445
446 let d = engine.authorize("alice", &Resource::new("orders"), &Action::Delete);
448 assert!(d.requires_approval_check());
449 assert!(d.reason().unwrap().contains("requires explicit approval"));
450
451 assert!(engine.authorize("alice", &Resource::new("orders"), &Action::Read).is_allowed());
453 }
454
455 #[test]
456 fn approval_required_wildcard_action() {
457 let mut engine = AuthzEngineBuilder::new()
458 .add_role(Role::admin())
459 .assign_role("alice", "admin")
460 .require_approval(Some("payments".to_owned()), None)
461 .build();
462
463 let d = engine.authorize("alice", &Resource::new("payments"), &Action::Read);
465 assert!(d.requires_approval_check());
466 }
467
468 #[test]
471 fn authorize_records_audit() {
472 let mut engine = basic_engine();
473 engine.authorize("alice", &Resource::new("orders"), &Action::Read);
474
475 assert_eq!(engine.audit_log().len(), 1);
476 let records = engine.query_audit(&AuditFilter::new().actor("alice"));
477 assert_eq!(records.len(), 1);
478 assert!(records[0].decision().is_allowed());
479 }
480
481 #[test]
482 fn denied_operations_also_audited() {
483 let mut engine = basic_engine();
484 engine.authorize("bob", &Resource::new("orders"), &Action::Delete);
485
486 let records = engine.query_audit(&AuditFilter::new().actor("bob"));
487 assert_eq!(records.len(), 1);
488 assert!(records[0].decision().is_denied());
489 }
490
491 #[test]
494 fn add_role_at_runtime() {
495 let mut engine = basic_engine();
496 let custom = RoleBuilder::new("custom").default_level(PermissionLevel::Write).build();
497
498 engine.add_role(custom);
499 engine.assign_role("charlie", "custom").unwrap();
500
501 assert!(
502 engine.authorize("charlie", &Resource::new("orders"), &Action::Create).is_allowed()
503 );
504 }
505
506 #[test]
507 fn assign_invalid_role_returns_error() {
508 let mut engine = basic_engine();
509 let err = engine.assign_role("charlie", "nonexistent").unwrap_err();
510 assert!(err.to_string().contains("nonexistent"));
511 }
512
513 #[test]
514 fn remove_role_denies_access() {
515 let mut engine = basic_engine();
516 assert!(engine.authorize("alice", &Resource::new("orders"), &Action::Read).is_allowed());
517
518 engine.remove_role("alice");
519
520 assert!(engine.authorize("alice", &Resource::new("orders"), &Action::Read).is_denied());
521 }
522
523 #[test]
524 fn actor_role_accessor() {
525 let engine = basic_engine();
526 assert_eq!(engine.actor_role("alice"), Some("admin"));
527 assert_eq!(engine.actor_role("ghost"), None);
528 }
529
530 #[test]
533 fn engine_redact_uses_config() {
534 let engine = AuthzEngineBuilder::new().build();
535 let mut value = serde_json::json!({
536 "name": "Test",
537 "password": "secret"
538 });
539
540 engine.redact(&mut value);
541 assert_eq!(value["password"], "[REDACTED]");
542 assert_eq!(value["name"], "Test");
543 }
544
545 #[test]
548 fn builder_default() {
549 let builder = AuthzEngineBuilder::default();
550 let engine = builder.build();
551 assert_eq!(engine.audit_log().max_size(), DEFAULT_AUDIT_MAX_SIZE);
552 }
553
554 #[test]
555 fn builder_custom_audit_size() {
556 let engine = AuthzEngineBuilder::new().audit_max_size(50).build();
557 assert_eq!(engine.audit_log().max_size(), 50);
558 }
559
560 #[test]
561 fn builder_custom_redaction() {
562 let config = RedactionConfig::with_fields(["custom_field"]);
563 let engine = AuthzEngineBuilder::new().redaction_config(config).build();
564
565 assert!(engine.redaction_config().should_redact("custom_field"));
566 assert!(!engine.redaction_config().should_redact("password"));
567 }
568
569 #[test]
570 fn builder_build_checked_rejects_unknown_role_assignment() {
571 let err = AuthzEngineBuilder::new()
572 .add_role(Role::admin())
573 .assign_role("alice", "missing")
574 .build_checked()
575 .unwrap_err();
576
577 assert_eq!(err, AuthzError::invalid_role("missing"));
578 }
579
580 #[test]
583 fn full_flow_assign_authorize_audit() {
584 let mut engine = AuthzEngineBuilder::new()
585 .add_role(Role::admin())
586 .add_role(Role::viewer())
587 .assign_role("alice", "admin")
588 .assign_role("bob", "viewer")
589 .rate_limit_rule(RateLimitRule::new("orders", 10, Duration::from_secs(60)))
590 .build();
591
592 let d = engine.authorize("alice", &Resource::new("orders"), &Action::Create);
594 assert!(d.is_allowed());
595
596 let d = engine.authorize("bob", &Resource::new("orders"), &Action::Create);
598 assert!(d.is_denied());
599
600 assert_eq!(engine.audit_log().len(), 2);
602
603 let alice_records = engine.query_audit(&AuditFilter::new().actor("alice"));
604 assert_eq!(alice_records.len(), 1);
605 assert!(alice_records[0].decision().is_allowed());
606
607 let bob_records = engine.query_audit(&AuditFilter::new().actor("bob"));
608 assert_eq!(bob_records.len(), 1);
609 assert!(bob_records[0].decision().is_denied());
610 }
611
612 #[test]
613 fn rate_limiting_integration() {
614 let mut engine = AuthzEngineBuilder::new()
615 .add_role(Role::admin())
616 .assign_role("alice", "admin")
617 .rate_limit_rule(RateLimitRule::new("orders", 3, Duration::from_secs(60)))
618 .build();
619
620 for _ in 0..3 {
621 assert!(
622 engine.authorize("alice", &Resource::new("orders"), &Action::Read).is_allowed()
623 );
624 }
625
626 let d = engine.authorize("alice", &Resource::new("orders"), &Action::Read);
627 assert!(d.is_denied());
628
629 assert_eq!(engine.audit_log().len(), 4);
631 }
632
633 #[test]
634 fn rate_limiter_accessors() {
635 let mut engine = AuthzEngineBuilder::new()
636 .rate_limit_rule(RateLimitRule::new("orders", 5, Duration::from_secs(60)))
637 .build();
638
639 assert_eq!(engine.rate_limiter().rule_count(), 1);
640
641 engine.rate_limiter_mut().add_rule(RateLimitRule::new(
643 "customers",
644 10,
645 Duration::from_secs(60),
646 ));
647 assert_eq!(engine.rate_limiter().rule_count(), 2);
648 }
649
650 #[test]
651 fn require_approval_at_runtime() {
652 let mut engine =
653 AuthzEngineBuilder::new().add_role(Role::admin()).assign_role("alice", "admin").build();
654
655 assert!(engine.authorize("alice", &Resource::new("orders"), &Action::Delete).is_allowed());
657
658 engine.require_approval(Some("orders".to_owned()), Some(Action::Delete));
660
661 let d = engine.authorize("alice", &Resource::new("orders"), &Action::Delete);
663 assert!(d.requires_approval_check());
664 }
665}