1use async_trait::async_trait;
6use parking_lot::RwLock;
7use serde_yaml;
8use std::collections::HashMap;
9use std::sync::Arc;
10use std::time::{Duration, SystemTime};
11use tokio::time::Instant;
12
13use super::types::*;
14use super::{PolicyEnforcementPoint, ResourceAccessConfig};
15use crate::secrets::{SecretError, SecretStore};
16use crate::types::security::Capability;
17use crate::types::*;
18use serde_json::Value;
19
20#[async_trait]
22pub trait PolicyEngine: Send + Sync {
23 async fn evaluate_policy(
25 &self,
26 agent_id: &str,
27 input: &serde_json::Value,
28 ) -> Result<PolicyDecision, PolicyError>;
29
30 async fn check_capability(
32 &self,
33 agent_id: &str,
34 capability: &Capability,
35 ) -> Result<PolicyDecision, PolicyError>;
36}
37
38#[derive(Debug, Clone, PartialEq)]
40pub enum PolicyDecision {
41 Allow,
42 Deny,
43}
44
45#[derive(Clone)]
47pub struct OpaPolicyEngine {
48 opa_client: OpaClient,
49}
50
51impl OpaPolicyEngine {
52 pub fn new() -> Self {
54 Self {
55 opa_client: OpaClient::new(),
56 }
57 }
58}
59
60impl Default for OpaPolicyEngine {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66#[async_trait]
67impl PolicyEngine for OpaPolicyEngine {
68 async fn evaluate_policy(
69 &self,
70 agent_id: &str,
71 input: &serde_json::Value,
72 ) -> Result<PolicyDecision, PolicyError> {
73 let query_input = serde_json::json!({
74 "input": {
75 "agent_id": agent_id,
76 "data": input
77 }
78 });
79
80 let query = "data.symbiont.main.allow".to_string();
81 let results: Value = self.opa_client.query(query, query_input).await?;
82
83 if results
84 .get("result")
85 .and_then(|r| r.as_bool())
86 .unwrap_or(false)
87 {
88 Ok(PolicyDecision::Allow)
89 } else {
90 Ok(PolicyDecision::Deny)
91 }
92 }
93
94 async fn check_capability(
95 &self,
96 agent_id: &str,
97 capability: &Capability,
98 ) -> Result<PolicyDecision, PolicyError> {
99 let input = serde_json::json!({
100 "input": {
101 "agent_id": agent_id,
102 "capability": capability,
103 }
104 });
105
106 let query = "data.symbiont.main.allow".to_string();
107 let results: Value = self.opa_client.query(query, input).await?;
108
109 if results
110 .get("result")
111 .and_then(|r| r.as_bool())
112 .unwrap_or(false)
113 {
114 Ok(PolicyDecision::Allow)
115 } else {
116 Ok(PolicyDecision::Deny)
117 }
118 }
119}
120
121#[derive(Clone)]
126struct OpaClient {
127 base_url: String,
128 client: reqwest::Client,
129}
130
131impl OpaClient {
132 fn new() -> Self {
133 let base_url = std::env::var("SYMBIONT_OPA_URL")
134 .unwrap_or_else(|_| "http://localhost:8181".to_string());
135 Self {
136 base_url,
137 client: reqwest::Client::builder()
138 .timeout(std::time::Duration::from_secs(5))
139 .build()
140 .unwrap_or_default(),
141 }
142 }
143
144 async fn query(
145 &self,
146 query: String,
147 input: serde_json::Value,
148 ) -> Result<serde_json::Value, PolicyError> {
149 let query_path = query.replace("data.", "");
150 let query_url = format!("{}/v1/data/{}", self.base_url, query_path);
151 let body = serde_json::json!({ "input": input });
152
153 match self.client.post(&query_url).json(&body).send().await {
154 Ok(resp) => {
155 if resp.status().is_success() {
156 let result: serde_json::Value = resp.json().await.map_err(|e| {
157 PolicyError::EvaluationFailed(format!(
158 "Failed to parse OPA response: {}",
159 e
160 ))
161 })?;
162 Ok(result
164 .get("result")
165 .cloned()
166 .unwrap_or(serde_json::json!(false)))
167 } else {
168 tracing::warn!("OPA returned HTTP {}: denying by default", resp.status());
169 Ok(serde_json::json!(false))
170 }
171 }
172 Err(e) => {
173 tracing::warn!(
175 "OPA unreachable at {}: {}. Denying by default (fail-closed)",
176 self.base_url,
177 e
178 );
179 Ok(serde_json::json!(false))
180 }
181 }
182 }
183}
184
185pub struct DefaultPolicyEnforcementPoint {
187 config: ResourceAccessConfig,
188 policies: Arc<RwLock<Vec<ResourceAccessPolicy>>>,
189 stats: Arc<RwLock<EnforcementStatistics>>,
190 decision_cache: Arc<RwLock<HashMap<String, CachedDecision>>>,
191 secrets: Option<Arc<dyn SecretStore + Send + Sync>>,
192}
193
194#[derive(Debug, Clone)]
196struct CachedDecision {
197 decision: AccessDecision,
198 expires_at: SystemTime,
199}
200
201impl DefaultPolicyEnforcementPoint {
202 pub async fn new(config: ResourceAccessConfig) -> Result<Self, PolicyError> {
204 let policies = Arc::new(RwLock::new(Vec::new()));
205 let stats = Arc::new(RwLock::new(EnforcementStatistics {
206 total_requests: 0,
207 decisions: HashMap::new(),
208 resource_types: HashMap::new(),
209 performance: PerformanceMetrics {
210 avg_evaluation_time_ms: 0.0,
211 p95_evaluation_time_ms: 0.0,
212 cache_hit_rate: if config.enable_caching {
213 Some(0.0)
214 } else {
215 None
216 },
217 policy_reloads: 0,
218 },
219 last_updated: SystemTime::now(),
220 }));
221 let decision_cache = Arc::new(RwLock::new(HashMap::new()));
222
223 let enforcement_point = Self {
224 config,
225 policies,
226 stats,
227 decision_cache,
228 secrets: None,
229 };
230
231 enforcement_point.load_default_policies().await?;
233
234 Ok(enforcement_point)
235 }
236
237 async fn load_default_policies(&self) -> Result<(), PolicyError> {
239 let policies_data: serde_yaml::Value = serde_yaml::from_str(DEFAULT_POLICIES_YAML)
240 .map_err(|e| PolicyError::InvalidPolicy {
241 reason: format!("Failed to parse default policies: {}", e).into(),
242 })?;
243
244 let policies = self.parse_policies_from_yaml(&policies_data)?;
245 *self.policies.write() = policies;
246
247 {
249 let mut stats = self.stats.write();
250 stats.performance.policy_reloads += 1;
251 stats.last_updated = SystemTime::now();
252 }
253
254 Ok(())
255 }
256
257 fn parse_policies_from_yaml(
259 &self,
260 data: &serde_yaml::Value,
261 ) -> Result<Vec<ResourceAccessPolicy>, PolicyError> {
262 let policies_array = data
263 .get("policies")
264 .and_then(|v| v.as_sequence())
265 .ok_or_else(|| PolicyError::InvalidPolicy {
266 reason: "Missing 'policies' array in YAML".into(),
267 })?;
268
269 let mut policies = Vec::new();
270
271 for policy_data in policies_array {
272 let policy = self.parse_single_policy(policy_data)?;
273 policies.push(policy);
274 }
275
276 policies.sort_by(|a, b| b.priority.cmp(&a.priority));
278
279 Ok(policies)
280 }
281
282 fn parse_single_policy(
284 &self,
285 data: &serde_yaml::Value,
286 ) -> Result<ResourceAccessPolicy, PolicyError> {
287 let id = data
288 .get("id")
289 .and_then(|v| v.as_str())
290 .ok_or_else(|| PolicyError::InvalidPolicy {
291 reason: "Policy missing 'id' field".into(),
292 })?
293 .to_string();
294
295 let name = data
296 .get("name")
297 .and_then(|v| v.as_str())
298 .unwrap_or(&id)
299 .to_string();
300
301 let description = data
302 .get("description")
303 .and_then(|v| v.as_str())
304 .unwrap_or("")
305 .to_string();
306
307 let version = data
308 .get("version")
309 .and_then(|v| v.as_str())
310 .unwrap_or("1.0.0")
311 .to_string();
312
313 let enabled = data
314 .get("enabled")
315 .and_then(|v| v.as_bool())
316 .unwrap_or(false); let priority = data
319 .get("priority")
320 .and_then(|v| v.as_u64())
321 .unwrap_or(1000) as u32;
322
323 let empty_rules = Vec::new();
324 let rules_data = data
325 .get("rules")
326 .and_then(|v| v.as_sequence())
327 .unwrap_or(&empty_rules);
328
329 let mut rules = Vec::new();
330 for rule_data in rules_data {
331 let rule = self.parse_rule(rule_data)?;
332 rules.push(rule);
333 }
334
335 rules.sort_by(|a, b| b.priority.cmp(&a.priority));
337
338 Ok(ResourceAccessPolicy {
339 id,
340 name,
341 description,
342 version,
343 enabled,
344 priority,
345 rules,
346 metadata: HashMap::new(),
347 created_at: SystemTime::now(),
348 updated_at: SystemTime::now(),
349 })
350 }
351
352 fn parse_rule(&self, data: &serde_yaml::Value) -> Result<ResourceAccessRule, PolicyError> {
354 let id = data
355 .get("id")
356 .and_then(|v| v.as_str())
357 .ok_or_else(|| PolicyError::InvalidPolicy {
358 reason: "Rule missing 'id' field".into(),
359 })?
360 .to_string();
361
362 let name = data
363 .get("name")
364 .and_then(|v| v.as_str())
365 .unwrap_or(&id)
366 .to_string();
367
368 let description = data
369 .get("description")
370 .and_then(|v| v.as_str())
371 .unwrap_or("")
372 .to_string();
373
374 let priority = data.get("priority").and_then(|v| v.as_u64()).unwrap_or(100) as u32;
375
376 let conditions = Vec::new();
378
379 let effect = self.parse_rule_effect(data.get("effect"))?;
381
382 Ok(ResourceAccessRule {
383 id,
384 name,
385 description,
386 conditions,
387 effect,
388 priority,
389 metadata: HashMap::new(),
390 })
391 }
392
393 fn parse_rule_effect(
395 &self,
396 data: Option<&serde_yaml::Value>,
397 ) -> Result<RuleEffect, PolicyError> {
398 let effect_data = data.ok_or_else(|| PolicyError::InvalidPolicy {
399 reason: "Rule missing 'effect' field".into(),
400 })?;
401
402 let effect_type = effect_data
403 .get("type")
404 .and_then(|v| v.as_str())
405 .ok_or_else(|| PolicyError::InvalidPolicy {
406 reason: "Effect missing 'type' field".into(),
407 })?;
408
409 match effect_type {
410 "Allow" => Ok(RuleEffect::Allow {
411 conditions: Vec::new(),
412 }),
413 "Deny" => {
414 let reason = effect_data
415 .get("reason")
416 .and_then(|v| v.as_str())
417 .unwrap_or("Access denied by policy")
418 .to_string();
419 Ok(RuleEffect::Deny { reason })
420 }
421 "Limit" => {
422 Ok(RuleEffect::Limit {
424 limits: ResourceConstraints {
425 max_concurrent_access: Some(10),
426 rate_limit: None,
427 transfer_limits: None,
428 time_restrictions: None,
429 },
430 })
431 }
432 "Audit" => Ok(RuleEffect::Audit {
433 level: AuditLevel::Info,
434 }),
435 "Escalate" => {
436 let to = effect_data
437 .get("to")
438 .and_then(|v| v.as_str())
439 .unwrap_or("administrator")
440 .to_string();
441 let reason = effect_data
442 .get("reason")
443 .and_then(|v| v.as_str())
444 .unwrap_or("Manual review required")
445 .to_string();
446 Ok(RuleEffect::Escalate { to, reason })
447 }
448 _ => Err(PolicyError::InvalidPolicy {
449 reason: format!("Unknown effect type: {}", effect_type).into(),
450 }),
451 }
452 }
453
454 async fn evaluate_access(
456 &self,
457 request: &ResourceAccessRequest,
458 ) -> Result<AccessDecision, PolicyError> {
459 let start_time = Instant::now();
460
461 if self.config.enable_caching {
463 let cache_key = self.generate_cache_key(request);
464 if let Some(cached) = self.check_cache(&cache_key) {
465 self.update_stats_cache_hit();
466 return Ok(cached.decision);
467 }
468 }
469
470 let mut decision = if self.config.default_deny {
472 AccessDecision {
473 decision: AccessResult::Deny,
474 reason: "Default deny policy".to_string(),
475 applied_rule: None,
476 conditions: Vec::new(),
477 expires_at: None,
478 metadata: HashMap::new(),
479 }
480 } else {
481 AccessDecision {
482 decision: AccessResult::Allow,
483 reason: "Default allow policy".to_string(),
484 applied_rule: None,
485 conditions: Vec::new(),
486 expires_at: None,
487 metadata: HashMap::new(),
488 }
489 };
490
491 let policies = {
493 let policies_guard = self.policies.read();
494 policies_guard.clone()
495 };
496
497 for policy in policies.iter() {
499 if !policy.enabled {
500 continue;
501 }
502
503 for rule in &policy.rules {
504 if self.rule_matches(rule, request).await? {
505 decision = self.apply_rule_effect(&rule.effect, &rule.id);
506 break;
507 }
508 }
509
510 if matches!(decision.decision, AccessResult::Allow | AccessResult::Deny) {
512 break;
513 }
514 }
515
516 if self.config.enable_caching {
518 let cache_key = self.generate_cache_key(request);
519 self.cache_decision(cache_key, decision.clone());
520 }
521
522 let eval_time = start_time.elapsed().as_millis() as f64;
524 self.update_stats(request, &decision, eval_time);
525
526 Ok(decision)
527 }
528
529 async fn rule_matches(
531 &self,
532 rule: &ResourceAccessRule,
533 _request: &ResourceAccessRequest,
534 ) -> Result<bool, PolicyError> {
535 let secret_valid = self.validate_secret_requirements(&rule.conditions).await?;
537 if !secret_valid {
538 return Ok(false);
539 }
540
541 Ok(true)
545 }
546
547 fn apply_rule_effect(&self, effect: &RuleEffect, rule_id: &str) -> AccessDecision {
549 match effect {
550 RuleEffect::Allow { conditions } => AccessDecision {
551 decision: AccessResult::Allow,
552 reason: "Access granted by policy rule".to_string(),
553 applied_rule: Some(rule_id.to_string()),
554 conditions: conditions.clone(),
555 expires_at: None,
556 metadata: HashMap::new(),
557 },
558 RuleEffect::Deny { reason } => AccessDecision {
559 decision: AccessResult::Deny,
560 reason: reason.clone(),
561 applied_rule: Some(rule_id.to_string()),
562 conditions: Vec::new(),
563 expires_at: None,
564 metadata: HashMap::new(),
565 },
566 RuleEffect::Limit { .. } => AccessDecision {
567 decision: AccessResult::Conditional,
568 reason: "Access granted with limits".to_string(),
569 applied_rule: Some(rule_id.to_string()),
570 conditions: Vec::new(),
571 expires_at: None,
572 metadata: HashMap::new(),
573 },
574 RuleEffect::Audit { .. } => AccessDecision {
575 decision: AccessResult::Allow,
576 reason: "Access granted with audit requirement".to_string(),
577 applied_rule: Some(rule_id.to_string()),
578 conditions: vec![AccessCondition {
579 condition_type: ConditionType::AuditRequired,
580 parameters: HashMap::new(),
581 timeout: None,
582 blocking: false,
583 }],
584 expires_at: None,
585 metadata: HashMap::new(),
586 },
587 RuleEffect::Escalate { to, reason } => AccessDecision {
588 decision: AccessResult::Escalate,
589 reason: reason.clone(),
590 applied_rule: Some(rule_id.to_string()),
591 conditions: Vec::new(),
592 expires_at: None,
593 metadata: {
594 let mut map = HashMap::new();
595 map.insert("escalate_to".to_string(), to.clone());
596 map
597 },
598 },
599 }
600 }
601
602 fn generate_cache_key(&self, request: &ResourceAccessRequest) -> String {
604 let resource_type_id = match request.resource_type {
605 ResourceType::File => 0u8,
606 ResourceType::Network => 1u8,
607 ResourceType::Command => 2u8,
608 ResourceType::Database => 3u8,
609 ResourceType::Environment => 4u8,
610 ResourceType::Agent => 5u8,
611 ResourceType::Custom(_) => 6u8,
612 };
613 let access_type_id = match request.access_type {
614 AccessType::Read => 0u8,
615 AccessType::Write => 1u8,
616 AccessType::Execute => 2u8,
617 AccessType::Delete => 3u8,
618 AccessType::Create => 4u8,
619 AccessType::Modify => 5u8,
620 AccessType::List => 6u8,
621 AccessType::Connect => 7u8,
622 };
623 format!(
624 "{}:{}:{}:{:?}",
625 resource_type_id, request.resource_id, access_type_id, request.context.security_level
626 )
627 }
628
629 fn check_cache(&self, key: &str) -> Option<CachedDecision> {
631 let cache = self.decision_cache.read();
632 if let Some(cached) = cache.get(key) {
633 if cached.expires_at > SystemTime::now() {
634 return Some(cached.clone());
635 }
636 }
637 None
638 }
639
640 fn cache_decision(&self, key: String, decision: AccessDecision) {
642 let expires_at = SystemTime::now() + Duration::from_secs(self.config.cache_ttl_secs);
643 let cached = CachedDecision {
644 decision,
645 expires_at,
646 };
647 self.decision_cache.write().insert(key, cached);
648 }
649
650 fn update_stats(
652 &self,
653 request: &ResourceAccessRequest,
654 decision: &AccessDecision,
655 eval_time_ms: f64,
656 ) {
657 let mut stats = self.stats.write();
658 stats.total_requests += 1;
659 *stats
660 .decisions
661 .entry(decision.decision.clone())
662 .or_insert(0) += 1;
663 *stats
664 .resource_types
665 .entry(request.resource_type.clone())
666 .or_insert(0) += 1;
667
668 let total_time = stats.performance.avg_evaluation_time_ms
670 * (stats.total_requests - 1) as f64
671 + eval_time_ms;
672 stats.performance.avg_evaluation_time_ms = total_time / stats.total_requests as f64;
673
674 stats.last_updated = SystemTime::now();
675 }
676
677 fn update_stats_cache_hit(&self) {
679 let mut stats = self.stats.write();
680 if let Some(ref mut hit_rate) = stats.performance.cache_hit_rate {
681 *hit_rate = (*hit_rate * 0.9) + (1.0 * 0.1);
683 }
684 }
685
686 pub fn set_secrets(&mut self, secrets: Arc<dyn SecretStore + Send + Sync>) {
688 self.secrets = Some(secrets);
689 }
690
691 async fn validate_secret_requirements(
693 &self,
694 conditions: &[RuleCondition],
695 ) -> Result<bool, PolicyError> {
696 for condition in conditions {
697 if let RuleCondition::SecretMatch {
698 secret_name,
699 permissions: _,
700 } = condition
701 {
702 if let Some(ref secrets) = self.secrets {
703 match secrets.get_secret(secret_name).await {
704 Ok(_) => {
705 continue;
707 }
708 Err(SecretError::NotFound { .. }) => {
709 return Ok(false);
710 }
711 Err(SecretError::PermissionDenied { .. }) => {
712 return Ok(false);
713 }
714 Err(e) => {
715 return Err(PolicyError::EvaluationFailed(format!(
716 "Secret validation error: {}",
717 e
718 )));
719 }
720 }
721 } else {
722 return Ok(false);
724 }
725 }
726 }
727 Ok(true)
728 }
729}
730
731#[async_trait]
732impl PolicyEnforcementPoint for DefaultPolicyEnforcementPoint {
733 async fn check_resource_access(
734 &self,
735 _agent_id: AgentId,
736 resource: &ResourceAccessRequest,
737 ) -> Result<AccessDecision, PolicyError> {
738 self.evaluate_access(resource).await
739 }
740
741 async fn validate_resource_allocation(
742 &self,
743 _agent_id: AgentId,
744 allocation: &ResourceAllocationRequest,
745 ) -> Result<AllocationDecision, PolicyError> {
746 let decision = if allocation.requirements.max_memory_mb > 16384 {
748 AllocationResult::Escalate
749 } else if allocation.requirements.max_memory_mb > 4096 {
750 AllocationResult::Modified
751 } else {
752 AllocationResult::Approve
753 };
754
755 Ok(AllocationDecision {
756 decision,
757 reason: "Resource allocation validated".to_string(),
758 modified_requirements: None,
759 conditions: Vec::new(),
760 expires_at: None,
761 metadata: HashMap::new(),
762 })
763 }
764
765 async fn load_policies(&self, config: &ResourceAccessConfig) -> Result<(), PolicyError> {
766 if let Some(policy_path) = &config.policy_path {
767 let _ = policy_path;
769 self.load_default_policies().await
770 } else {
771 self.load_default_policies().await
772 }
773 }
774
775 async fn reload_policies(&self) -> Result<(), PolicyError> {
776 self.load_policies(&self.config.clone()).await
777 }
778
779 async fn get_enforcement_stats(&self) -> Result<EnforcementStatistics, PolicyError> {
780 Ok(self.stats.read().clone())
781 }
782}
783
784pub struct MockPolicyEnforcementPoint {
786 stats: Arc<RwLock<EnforcementStatistics>>,
787}
788
789impl Default for MockPolicyEnforcementPoint {
790 fn default() -> Self {
791 Self::new()
792 }
793}
794
795impl MockPolicyEnforcementPoint {
796 pub fn new() -> Self {
797 Self {
798 stats: Arc::new(RwLock::new(EnforcementStatistics {
799 total_requests: 0,
800 decisions: HashMap::new(),
801 resource_types: HashMap::new(),
802 performance: PerformanceMetrics {
803 avg_evaluation_time_ms: 1.0,
804 p95_evaluation_time_ms: 5.0,
805 cache_hit_rate: Some(0.95),
806 policy_reloads: 0,
807 },
808 last_updated: SystemTime::now(),
809 })),
810 }
811 }
812}
813
814#[async_trait]
815impl PolicyEnforcementPoint for MockPolicyEnforcementPoint {
816 async fn check_resource_access(
817 &self,
818 _agent_id: AgentId,
819 resource: &ResourceAccessRequest,
820 ) -> Result<AccessDecision, PolicyError> {
821 {
823 let mut stats = self.stats.write();
824 stats.total_requests += 1;
825 *stats
826 .resource_types
827 .entry(resource.resource_type.clone())
828 .or_insert(0) += 1;
829 }
830
831 let decision = match &resource.resource_type {
833 ResourceType::File => {
834 if resource.resource_id.contains("/etc/") || resource.resource_id.contains("/sys/")
835 {
836 AccessResult::Deny
837 } else {
838 AccessResult::Allow
839 }
840 }
841 ResourceType::Network => {
842 if resource.resource_id.contains("malicious") {
843 AccessResult::Deny
844 } else {
845 AccessResult::Allow
846 }
847 }
848 _ => AccessResult::Allow,
849 };
850
851 {
852 let mut stats = self.stats.write();
853 *stats.decisions.entry(decision.clone()).or_insert(0) += 1;
854 stats.last_updated = SystemTime::now();
855 }
856
857 Ok(AccessDecision {
858 decision,
859 reason: "Mock policy evaluation".to_string(),
860 applied_rule: Some("mock-rule".to_string()),
861 conditions: Vec::new(),
862 expires_at: None,
863 metadata: HashMap::new(),
864 })
865 }
866
867 async fn validate_resource_allocation(
868 &self,
869 _agent_id: AgentId,
870 _allocation: &ResourceAllocationRequest,
871 ) -> Result<AllocationDecision, PolicyError> {
872 Ok(AllocationDecision {
873 decision: AllocationResult::Approve,
874 reason: "Mock allocation approval".to_string(),
875 modified_requirements: None,
876 conditions: Vec::new(),
877 expires_at: None,
878 metadata: HashMap::new(),
879 })
880 }
881
882 async fn load_policies(&self, _config: &ResourceAccessConfig) -> Result<(), PolicyError> {
883 let mut stats = self.stats.write();
884 stats.performance.policy_reloads += 1;
885 Ok(())
886 }
887
888 async fn reload_policies(&self) -> Result<(), PolicyError> {
889 let mut stats = self.stats.write();
890 stats.performance.policy_reloads += 1;
891 Ok(())
892 }
893
894 async fn get_enforcement_stats(&self) -> Result<EnforcementStatistics, PolicyError> {
895 Ok(self.stats.read().clone())
896 }
897}
898
899#[cfg(test)]
900mod tests {
901 use super::*;
902
903 #[tokio::test]
904 async fn test_default_enforcement_point_creation() {
905 let config = ResourceAccessConfig::default();
906 let enforcement_point = DefaultPolicyEnforcementPoint::new(config).await;
907 assert!(enforcement_point.is_ok());
908 }
909
910 #[tokio::test]
911 async fn test_mock_enforcement_point() {
912 let enforcement_point = MockPolicyEnforcementPoint::new();
913 let agent_id = AgentId::new();
914
915 let request = ResourceAccessRequest {
916 resource_type: ResourceType::File,
917 resource_id: "/tmp/test.txt".to_string(),
918 access_type: AccessType::Read,
919 context: AccessContext {
920 agent_metadata: AgentMetadata {
921 version: "1.0.0".to_string(),
922 author: "test".to_string(),
923 description: "Test agent".to_string(),
924 capabilities: vec![],
925 dependencies: vec![],
926 resource_requirements: crate::types::agent::ResourceRequirements::default(),
927 security_requirements: crate::types::agent::SecurityRequirements::default(),
928 custom_fields: HashMap::new(),
929 },
930 security_level: SecurityTier::Tier1,
931 access_history: Vec::new(),
932 resource_usage: ResourceUsage::default(),
933 environment: HashMap::new(),
934 source_info: SourceInfo {
935 ip_address: None,
936 user_agent: None,
937 session_id: None,
938 request_id: "test-request".to_string(),
939 },
940 },
941 timestamp: SystemTime::now(),
942 };
943
944 let decision = enforcement_point
945 .check_resource_access(agent_id, &request)
946 .await
947 .unwrap();
948 assert_eq!(decision.decision, AccessResult::Allow);
949 }
950
951 #[tokio::test]
952 async fn test_mock_enforcement_point_deny() {
953 let enforcement_point = MockPolicyEnforcementPoint::new();
954 let agent_id = AgentId::new();
955
956 let request = ResourceAccessRequest {
957 resource_type: ResourceType::File,
958 resource_id: "/etc/passwd".to_string(),
959 access_type: AccessType::Read,
960 context: AccessContext {
961 agent_metadata: AgentMetadata {
962 version: "1.0.0".to_string(),
963 author: "test".to_string(),
964 description: "Test agent".to_string(),
965 capabilities: vec![],
966 dependencies: vec![],
967 resource_requirements: crate::types::agent::ResourceRequirements::default(),
968 security_requirements: crate::types::agent::SecurityRequirements::default(),
969 custom_fields: HashMap::new(),
970 },
971 security_level: SecurityTier::Tier1,
972 access_history: Vec::new(),
973 resource_usage: ResourceUsage::default(),
974 environment: HashMap::new(),
975 source_info: SourceInfo {
976 ip_address: None,
977 user_agent: None,
978 session_id: None,
979 request_id: "test-request".to_string(),
980 },
981 },
982 timestamp: SystemTime::now(),
983 };
984
985 let decision = enforcement_point
986 .check_resource_access(agent_id, &request)
987 .await
988 .unwrap();
989 assert_eq!(decision.decision, AccessResult::Deny);
990 }
991}