1#![allow(clippy::type_complexity)]
167use async_trait::async_trait;
168use std::fmt;
169use std::sync::Arc;
170
171#[derive(Debug, PartialEq, Clone)]
173pub enum CombineOp {
174 And,
175 Or,
176 Not,
177}
178
179impl fmt::Display for CombineOp {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 match self {
182 CombineOp::And => write!(f, "AND"),
183 CombineOp::Or => write!(f, "OR"),
184 CombineOp::Not => write!(f, "NOT"),
185 }
186 }
187}
188
189#[derive(Debug, Clone)]
199pub enum PolicyEvalResult {
200 Granted {
202 policy_type: String,
203 reason: Option<String>,
204 },
205 Denied { policy_type: String, reason: String },
207 Combined {
211 policy_type: String,
212 operation: CombineOp,
213 children: Vec<PolicyEvalResult>,
214 outcome: bool,
215 },
216}
217
218#[derive(Debug, Clone)]
261pub enum AccessEvaluation {
262 Granted {
264 policy_type: String,
266 reason: Option<String>,
268 trace: EvalTrace,
270 },
271 Denied {
273 trace: EvalTrace,
275 reason: String,
277 },
278}
279
280impl AccessEvaluation {
281 pub fn is_granted(&self) -> bool {
283 matches!(self, Self::Granted { .. })
284 }
285
286 pub fn to_result<E>(&self, error_fn: impl FnOnce(&str) -> E) -> Result<(), E> {
288 match self {
289 Self::Granted { .. } => Ok(()),
290 Self::Denied { reason, .. } => Err(error_fn(reason)),
291 }
292 }
293
294 pub fn display_trace(&self) -> String {
295 let trace = match self {
296 AccessEvaluation::Granted {
297 policy_type: _,
298 reason: _,
299 trace,
300 } => trace,
301 AccessEvaluation::Denied { reason: _, trace } => trace,
302 };
303
304 let trace_str = trace.format();
306 if trace_str == "No evaluation trace available" {
307 format!("{}\n(No evaluation trace available)", self)
308 } else {
309 format!("{}\nEvaluation Trace:\n{}", self, trace_str)
310 }
311 }
312}
313
314impl fmt::Display for AccessEvaluation {
316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317 match self {
318 Self::Granted {
319 policy_type,
320 reason,
321 trace: _,
322 } => {
323 match reason {
325 Some(r) => write!(f, "[GRANTED] by {} - {}", policy_type, r),
326 None => write!(f, "[GRANTED] by {}", policy_type),
327 }
328 }
329 Self::Denied { reason, trace: _ } => {
330 write!(f, "[Denied] - {}", reason)
331 }
332 }
333 }
334}
335
336#[derive(Debug, Clone, Default)]
339pub struct EvalTrace {
340 root: Option<PolicyEvalResult>,
341}
342
343impl EvalTrace {
344 pub fn new() -> Self {
345 Self { root: None }
346 }
347
348 pub fn with_root(result: PolicyEvalResult) -> Self {
349 Self { root: Some(result) }
350 }
351
352 pub fn set_root(&mut self, result: PolicyEvalResult) {
353 self.root = Some(result);
354 }
355
356 pub fn root(&self) -> Option<&PolicyEvalResult> {
357 self.root.as_ref()
358 }
359
360 pub fn format(&self) -> String {
362 match &self.root {
363 Some(root) => root.format(0),
364 None => "No evaluation trace available".to_string(),
365 }
366 }
367}
368
369impl PolicyEvalResult {
370 pub fn is_granted(&self) -> bool {
372 match self {
373 Self::Granted { .. } => true,
374 Self::Denied { .. } => false,
375 Self::Combined { outcome, .. } => *outcome,
376 }
377 }
378
379 pub fn reason(&self) -> Option<String> {
381 match self {
382 Self::Granted { reason, .. } => reason.clone(),
383 Self::Denied { reason, .. } => Some(reason.clone()),
384 Self::Combined { .. } => None,
385 }
386 }
387
388 pub fn format(&self, indent: usize) -> String {
390 let indent_str = " ".repeat(indent);
391
392 match self {
393 Self::Granted {
394 policy_type,
395 reason,
396 } => {
397 let reason_text = reason
398 .as_ref()
399 .map_or("".to_string(), |r| format!(": {}", r));
400 format!("{}✔ {} GRANTED{}", indent_str, policy_type, reason_text)
401 }
402 Self::Denied {
403 policy_type,
404 reason,
405 } => {
406 format!("{}✘ {} DENIED: {}", indent_str, policy_type, reason)
407 }
408 Self::Combined {
409 policy_type,
410 operation,
411 children,
412 outcome,
413 } => {
414 let outcome_char = if *outcome { "✔" } else { "✘" };
415 let mut result = format!(
416 "{}{} {} ({})",
417 indent_str, outcome_char, policy_type, operation
418 );
419
420 for child in children {
421 result.push_str(&format!("\n{}", child.format(indent + 2)));
422 }
423 result
424 }
425 }
426 }
427}
428
429impl fmt::Display for PolicyEvalResult {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 let tree = self.format(0);
432 write!(f, "{}", tree)
433 }
434}
435
436#[async_trait]
440pub trait Policy<Subject, Resource, Action, Context>: Send + Sync {
441 async fn evaluate_access(
454 &self,
455 subject: &Subject,
456 action: &Action,
457 resource: &Resource,
458 context: &Context,
459 ) -> PolicyEvalResult;
460
461 fn policy_type(&self) -> String;
463}
464
465#[derive(Clone)]
470pub struct PermissionChecker<S, R, A, C> {
471 policies: Vec<Arc<dyn Policy<S, R, A, C>>>,
472}
473
474impl<S, R, A, C> Default for PermissionChecker<S, R, A, C> {
475 fn default() -> Self {
476 Self::new()
477 }
478}
479
480impl<S, R, A, C> PermissionChecker<S, R, A, C> {
481 pub fn new() -> Self {
483 Self {
484 policies: Vec::new(),
485 }
486 }
487
488 pub fn add_policy<P: Policy<S, R, A, C> + 'static>(&mut self, policy: P) {
494 self.policies.push(Arc::new(policy));
495 }
496
497 #[tracing::instrument(skip_all)]
502 pub async fn evaluate_access(
503 &self,
504 subject: &S,
505 action: &A,
506 resource: &R,
507 context: &C,
508 ) -> AccessEvaluation {
509 if self.policies.is_empty() {
510 tracing::debug!("No policies configured");
511 let result = PolicyEvalResult::Denied {
512 policy_type: "PermissionChecker".to_string(),
513 reason: "No policies configured".to_string(),
514 };
515
516 return AccessEvaluation::Denied {
517 trace: EvalTrace::with_root(result),
518 reason: "No policies configured".to_string(),
519 };
520 }
521 tracing::trace!(num_policies = self.policies.len(), "Checking access");
522
523 let mut policy_results = Vec::new();
524
525 for policy in &self.policies {
527 let result = policy
528 .evaluate_access(subject, action, resource, context)
529 .await;
530 let result_passes = result.is_granted();
531 policy_results.push(result.clone());
532
533 if result_passes {
535 let combined = PolicyEvalResult::Combined {
536 policy_type: "PermissionChecker".to_string(),
537 operation: CombineOp::Or,
538 children: policy_results,
539 outcome: true,
540 };
541
542 return AccessEvaluation::Granted {
543 policy_type: policy.policy_type(),
544 reason: result.reason(),
545 trace: EvalTrace::with_root(combined),
546 };
547 }
548 }
549
550 tracing::trace!("No policies allowed access, returning Forbidden");
552 let combined = PolicyEvalResult::Combined {
553 policy_type: "PermissionChecker".to_string(),
554 operation: CombineOp::Or,
555 children: policy_results,
556 outcome: false,
557 };
558
559 AccessEvaluation::Denied {
560 trace: EvalTrace::with_root(combined),
561 reason: "All policies denied access".to_string(),
562 }
563 }
564}
565
566#[derive(Debug, Clone, PartialEq, Eq)]
570pub enum Effect {
571 Allow,
572 Deny,
573}
574
575struct InternalPolicy<S, R, A, C> {
577 name: String,
578 effect: Effect,
579 predicate: Box<dyn Fn(&S, &A, &R, &C) -> bool + Send + Sync>,
581}
582
583#[async_trait]
584impl<S, R, A, C> Policy<S, R, A, C> for InternalPolicy<S, R, A, C>
585where
586 S: Send + Sync,
587 R: Send + Sync,
588 A: Send + Sync,
589 C: Send + Sync,
590{
591 async fn evaluate_access(
592 &self,
593 subject: &S,
594 action: &A,
595 resource: &R,
596 context: &C,
597 ) -> PolicyEvalResult {
598 if (self.predicate)(subject, action, resource, context) {
599 match self.effect {
600 Effect::Allow => PolicyEvalResult::Granted {
601 policy_type: self.name.clone(),
602 reason: Some("Policy allowed access".into()),
603 },
604 Effect::Deny => PolicyEvalResult::Denied {
605 policy_type: self.name.clone(),
606 reason: "Policy denied access".into(),
607 },
608 }
609 } else {
610 PolicyEvalResult::Denied {
612 policy_type: self.name.clone(),
613 reason: "Policy predicate did not match".into(),
614 }
615 }
616 }
617 fn policy_type(&self) -> String {
618 self.name.clone()
619 }
620}
621
622#[async_trait]
625impl<S, R, A, C> Policy<S, R, A, C> for Box<dyn Policy<S, R, A, C>>
626where
627 S: Send + Sync,
628 R: Send + Sync,
629 A: Send + Sync,
630 C: Send + Sync,
631{
632 async fn evaluate_access(
633 &self,
634 subject: &S,
635 action: &A,
636 resource: &R,
637 context: &C,
638 ) -> PolicyEvalResult {
639 (**self)
640 .evaluate_access(subject, action, resource, context)
641 .await
642 }
643
644 fn policy_type(&self) -> String {
645 (**self).policy_type()
646 }
647}
648
649pub struct PolicyBuilder<S, R, A, C>
655where
656 S: Send + Sync + 'static,
657 R: Send + Sync + 'static,
658 A: Send + Sync + 'static,
659 C: Send + Sync + 'static,
660{
661 name: String,
662 effect: Effect,
663 subject_pred: Option<Box<dyn Fn(&S) -> bool + Send + Sync>>,
664 action_pred: Option<Box<dyn Fn(&A) -> bool + Send + Sync>>,
665 resource_pred: Option<Box<dyn Fn(&R) -> bool + Send + Sync>>,
666 context_pred: Option<Box<dyn Fn(&C) -> bool + Send + Sync>>,
667 extra_condition: Option<Box<dyn Fn(&S, &A, &R, &C) -> bool + Send + Sync>>,
669}
670
671impl<Subject, Resource, Action, Context> PolicyBuilder<Subject, Resource, Action, Context>
672where
673 Subject: Send + Sync + 'static,
674 Resource: Send + Sync + 'static,
675 Action: Send + Sync + 'static,
676 Context: Send + Sync + 'static,
677{
678 pub fn new(name: impl Into<String>) -> Self {
680 Self {
681 name: name.into(),
682 effect: Effect::Allow,
683 subject_pred: None,
684 action_pred: None,
685 resource_pred: None,
686 context_pred: None,
687 extra_condition: None,
688 }
689 }
690
691 pub fn effect(mut self, effect: Effect) -> Self {
694 self.effect = effect;
695 self
696 }
697
698 pub fn subjects<F>(mut self, pred: F) -> Self
700 where
701 F: Fn(&Subject) -> bool + Send + Sync + 'static,
702 {
703 self.subject_pred = Some(Box::new(pred));
704 self
705 }
706
707 pub fn actions<F>(mut self, pred: F) -> Self
709 where
710 F: Fn(&Action) -> bool + Send + Sync + 'static,
711 {
712 self.action_pred = Some(Box::new(pred));
713 self
714 }
715
716 pub fn resources<F>(mut self, pred: F) -> Self
718 where
719 F: Fn(&Resource) -> bool + Send + Sync + 'static,
720 {
721 self.resource_pred = Some(Box::new(pred));
722 self
723 }
724
725 pub fn context<F>(mut self, pred: F) -> Self
727 where
728 F: Fn(&Context) -> bool + Send + Sync + 'static,
729 {
730 self.context_pred = Some(Box::new(pred));
731 self
732 }
733
734 pub fn when<F>(mut self, pred: F) -> Self
736 where
737 F: Fn(&Subject, &Action, &Resource, &Context) -> bool + Send + Sync + 'static,
738 {
739 self.extra_condition = Some(Box::new(pred));
740 self
741 }
742
743 pub fn build(self) -> Box<dyn Policy<Subject, Resource, Action, Context>> {
745 let effect = self.effect;
746 let subject_pred = self.subject_pred;
747 let action_pred = self.action_pred;
748 let resource_pred = self.resource_pred;
749 let context_pred = self.context_pred;
750 let extra_condition = self.extra_condition;
751
752 let predicate = Box::new(move |s: &Subject, a: &Action, r: &Resource, c: &Context| {
753 subject_pred.as_ref().is_none_or(|f| f(s))
754 && action_pred.as_ref().is_none_or(|f| f(a))
755 && resource_pred.as_ref().is_none_or(|f| f(r))
756 && context_pred.as_ref().is_none_or(|f| f(c))
757 && extra_condition.as_ref().is_none_or(|f| f(s, a, r, c))
758 });
759
760 Box::new(InternalPolicy {
761 name: self.name,
762 effect,
763 predicate,
764 })
765 }
766}
767
768pub struct RbacPolicy<S, F1, F2> {
773 required_roles_resolver: F1,
774 user_roles_resolver: F2,
775 _marker: std::marker::PhantomData<S>,
776}
777
778impl<S, F1, F2> RbacPolicy<S, F1, F2> {
779 pub fn new(required_roles_resolver: F1, user_roles_resolver: F2) -> Self {
780 Self {
781 required_roles_resolver,
782 user_roles_resolver,
783 _marker: std::marker::PhantomData,
784 }
785 }
786}
787
788#[async_trait]
789impl<S, R, A, C, F1, F2> Policy<S, R, A, C> for RbacPolicy<S, F1, F2>
790where
791 S: Sync + Send,
792 R: Sync + Send,
793 A: Sync + Send,
794 C: Sync + Send,
795 F1: Fn(&R, &A) -> Vec<uuid::Uuid> + Sync + Send,
796 F2: Fn(&S) -> Vec<uuid::Uuid> + Sync + Send,
797{
798 async fn evaluate_access(
799 &self,
800 subject: &S,
801 action: &A,
802 resource: &R,
803 _context: &C,
804 ) -> PolicyEvalResult {
805 let required_roles = (self.required_roles_resolver)(resource, action);
806 let user_roles = (self.user_roles_resolver)(subject);
807 let has_role = required_roles.iter().any(|role| user_roles.contains(role));
808
809 if has_role {
810 PolicyEvalResult::Granted {
811 policy_type: Policy::<S, R, A, C>::policy_type(self),
812 reason: Some("User has required role".to_string()),
813 }
814 } else {
815 PolicyEvalResult::Denied {
816 policy_type: Policy::<S, R, A, C>::policy_type(self),
817 reason: "User doesn't have required role".to_string(),
818 }
819 }
820 }
821
822 fn policy_type(&self) -> String {
823 "RbacPolicy".to_string()
824 }
825}
826
827pub struct AbacPolicy<S, R, A, C, F> {
893 condition: F,
894 _marker: std::marker::PhantomData<(S, R, A, C)>,
895}
896
897impl<S, R, A, C, F> AbacPolicy<S, R, A, C, F> {
898 pub fn new(condition: F) -> Self {
899 Self {
900 condition,
901 _marker: std::marker::PhantomData,
902 }
903 }
904}
905
906#[async_trait]
907impl<S, R, A, C, F> Policy<S, R, A, C> for AbacPolicy<S, R, A, C, F>
908where
909 S: Sync + Send,
910 R: Sync + Send,
911 A: Sync + Send,
912 C: Sync + Send,
913 F: Fn(&S, &R, &A, &C) -> bool + Sync + Send,
914{
915 async fn evaluate_access(
916 &self,
917 subject: &S,
918 action: &A,
919 resource: &R,
920 context: &C,
921 ) -> PolicyEvalResult {
922 let condition_met = (self.condition)(subject, resource, action, context);
923
924 if condition_met {
925 PolicyEvalResult::Granted {
926 policy_type: self.policy_type(),
927 reason: Some("Condition evaluated to true".to_string()),
928 }
929 } else {
930 PolicyEvalResult::Denied {
931 policy_type: self.policy_type(),
932 reason: "Condition evaluated to false".to_string(),
933 }
934 }
935 }
936
937 fn policy_type(&self) -> String {
938 "AbacPolicy".to_string()
939 }
940}
941
942#[async_trait]
946pub trait RelationshipResolver<S, R>: Send + Sync {
947 async fn has_relationship(&self, subject: &S, resource: &R, relationship: &str) -> bool;
948}
949
950pub struct RebacPolicy<S, R, A, C, RG> {
1022 pub relationship: String,
1023 pub resolver: RG,
1024 _marker: std::marker::PhantomData<(S, R, A, C)>,
1025}
1026
1027impl<S, R, A, C, RG> RebacPolicy<S, R, A, C, RG> {
1028 pub fn new(relationship: impl Into<String>, resolver: RG) -> Self {
1030 Self {
1031 relationship: relationship.into(),
1032 resolver,
1033 _marker: std::marker::PhantomData,
1034 }
1035 }
1036}
1037
1038#[async_trait]
1039impl<S, R, A, C, RG> Policy<S, R, A, C> for RebacPolicy<S, R, A, C, RG>
1040where
1041 S: Sync + Send,
1042 R: Sync + Send,
1043 A: Sync + Send,
1044 C: Sync + Send,
1045 RG: RelationshipResolver<S, R> + Send + Sync,
1046{
1047 async fn evaluate_access(
1048 &self,
1049 subject: &S,
1050 _action: &A,
1051 resource: &R,
1052 _context: &C,
1053 ) -> PolicyEvalResult {
1054 let has_relationship = self
1055 .resolver
1056 .has_relationship(subject, resource, &self.relationship)
1057 .await;
1058
1059 if has_relationship {
1060 PolicyEvalResult::Granted {
1061 policy_type: self.policy_type(),
1062 reason: Some(format!(
1063 "Subject has '{}' relationship with resource",
1064 self.relationship
1065 )),
1066 }
1067 } else {
1068 PolicyEvalResult::Denied {
1069 policy_type: self.policy_type(),
1070 reason: format!(
1071 "Subject does not have '{}' relationship with resource",
1072 self.relationship
1073 ),
1074 }
1075 }
1076 }
1077
1078 fn policy_type(&self) -> String {
1079 "RebacPolicy".to_string()
1080 }
1081}
1082
1083pub struct AndPolicy<S, R, A, C> {
1092 policies: Vec<Arc<dyn Policy<S, R, A, C>>>,
1093}
1094
1095#[derive(Debug, Copy, Clone)]
1097pub struct EmptyPoliciesError(pub &'static str);
1098
1099impl<S, R, A, C> AndPolicy<S, R, A, C> {
1100 pub fn try_new(policies: Vec<Arc<dyn Policy<S, R, A, C>>>) -> Result<Self, EmptyPoliciesError> {
1101 if policies.is_empty() {
1102 Err(EmptyPoliciesError(
1103 "AndPolicy must have at least one policy",
1104 ))
1105 } else {
1106 Ok(Self { policies })
1107 }
1108 }
1109}
1110
1111#[async_trait]
1112impl<S, R, A, C> Policy<S, R, A, C> for AndPolicy<S, R, A, C>
1113where
1114 S: Sync + Send,
1115 R: Sync + Send,
1116 A: Sync + Send,
1117 C: Sync + Send,
1118{
1119 fn policy_type(&self) -> String {
1121 "AndPolicy".to_string()
1122 }
1123
1124 async fn evaluate_access(
1125 &self,
1126 subject: &S,
1127 action: &A,
1128 resource: &R,
1129 context: &C,
1130 ) -> PolicyEvalResult {
1131 let mut children_results = Vec::new();
1132
1133 for policy in &self.policies {
1134 let result = policy
1135 .evaluate_access(subject, action, resource, context)
1136 .await;
1137 children_results.push(result.clone());
1138
1139 if !result.is_granted() {
1141 return PolicyEvalResult::Combined {
1142 policy_type: self.policy_type(),
1143 operation: CombineOp::And,
1144 children: children_results,
1145 outcome: false,
1146 };
1147 }
1148 }
1149
1150 PolicyEvalResult::Combined {
1152 policy_type: self.policy_type(),
1153 operation: CombineOp::And,
1154 children: children_results,
1155 outcome: true,
1156 }
1157 }
1158}
1159
1160pub struct OrPolicy<S, R, A, C> {
1165 policies: Vec<Arc<dyn Policy<S, R, A, C>>>,
1166}
1167
1168impl<S, R, A, C> OrPolicy<S, R, A, C> {
1169 pub fn try_new(policies: Vec<Arc<dyn Policy<S, R, A, C>>>) -> Result<Self, EmptyPoliciesError> {
1170 if policies.is_empty() {
1171 Err(EmptyPoliciesError("OrPolicy must have at least one policy"))
1172 } else {
1173 Ok(Self { policies })
1174 }
1175 }
1176}
1177
1178#[async_trait]
1179impl<S, R, A, C> Policy<S, R, A, C> for OrPolicy<S, R, A, C>
1180where
1181 S: Sync + Send,
1182 R: Sync + Send,
1183 A: Sync + Send,
1184 C: Sync + Send,
1185{
1186 fn policy_type(&self) -> String {
1188 "OrPolicy".to_string()
1189 }
1190 async fn evaluate_access(
1191 &self,
1192 subject: &S,
1193 action: &A,
1194 resource: &R,
1195 context: &C,
1196 ) -> PolicyEvalResult {
1197 let mut children_results = Vec::new();
1198
1199 for policy in &self.policies {
1200 let result = policy
1201 .evaluate_access(subject, action, resource, context)
1202 .await;
1203 children_results.push(result.clone());
1204
1205 if result.is_granted() {
1207 return PolicyEvalResult::Combined {
1208 policy_type: self.policy_type(),
1209 operation: CombineOp::Or,
1210 children: children_results,
1211 outcome: true,
1212 };
1213 }
1214 }
1215
1216 PolicyEvalResult::Combined {
1218 policy_type: self.policy_type(),
1219 operation: CombineOp::Or,
1220 children: children_results,
1221 outcome: false,
1222 }
1223 }
1224}
1225
1226pub struct NotPolicy<S, R, A, C> {
1231 policy: Arc<dyn Policy<S, R, A, C>>,
1232}
1233
1234impl<S, R, A, C> NotPolicy<S, R, A, C> {
1235 pub fn new(policy: impl Policy<S, R, A, C> + 'static) -> Self {
1236 Self {
1237 policy: Arc::new(policy),
1238 }
1239 }
1240}
1241
1242#[async_trait]
1243impl<S, R, A, C> Policy<S, R, A, C> for NotPolicy<S, R, A, C>
1244where
1245 S: Sync + Send,
1246 R: Sync + Send,
1247 A: Sync + Send,
1248 C: Sync + Send,
1249{
1250 fn policy_type(&self) -> String {
1252 "NotPolicy".to_string()
1253 }
1254
1255 async fn evaluate_access(
1256 &self,
1257 subject: &S,
1258 action: &A,
1259 resource: &R,
1260 context: &C,
1261 ) -> PolicyEvalResult {
1262 let inner_result = self
1263 .policy
1264 .evaluate_access(subject, action, resource, context)
1265 .await;
1266
1267 PolicyEvalResult::Combined {
1268 policy_type: Policy::<S, R, A, C>::policy_type(self),
1269 operation: CombineOp::Not,
1270 children: vec![inner_result.clone()],
1271 outcome: !inner_result.is_granted(),
1272 }
1273 }
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278 use super::*;
1279
1280 #[derive(Debug, Clone)]
1282 pub struct TestSubject {
1283 pub id: uuid::Uuid,
1284 }
1285
1286 #[derive(Debug, Clone)]
1287 pub struct TestResource {
1288 pub id: uuid::Uuid,
1289 }
1290
1291 #[derive(Debug, Clone)]
1292 pub struct TestAction;
1293
1294 #[derive(Debug, Clone)]
1295 pub struct TestContext;
1296
1297 struct AlwaysAllowPolicy;
1299
1300 #[async_trait]
1301 impl Policy<TestSubject, TestResource, TestAction, TestContext> for AlwaysAllowPolicy {
1302 async fn evaluate_access(
1303 &self,
1304 _subject: &TestSubject,
1305 _action: &TestAction,
1306 _resource: &TestResource,
1307 _context: &TestContext,
1308 ) -> PolicyEvalResult {
1309 PolicyEvalResult::Granted {
1310 policy_type: self.policy_type(),
1311 reason: Some("Always allow policy".to_string()),
1312 }
1313 }
1314
1315 fn policy_type(&self) -> String {
1316 "AlwaysAllowPolicy".to_string()
1317 }
1318 }
1319
1320 struct AlwaysDenyPolicy(&'static str);
1322
1323 #[async_trait]
1324 impl Policy<TestSubject, TestResource, TestAction, TestContext> for AlwaysDenyPolicy {
1325 async fn evaluate_access(
1326 &self,
1327 _subject: &TestSubject,
1328 _action: &TestAction,
1329 _resource: &TestResource,
1330 _context: &TestContext,
1331 ) -> PolicyEvalResult {
1332 PolicyEvalResult::Denied {
1333 policy_type: self.policy_type(),
1334 reason: self.0.to_string(),
1335 }
1336 }
1337
1338 fn policy_type(&self) -> String {
1339 "AlwaysDenyPolicy".to_string()
1340 }
1341 }
1342
1343 #[tokio::test]
1344 async fn test_no_policies() {
1345 let checker =
1346 PermissionChecker::<TestSubject, TestResource, TestAction, TestContext>::new();
1347
1348 let subject = TestSubject {
1349 id: uuid::Uuid::new_v4(),
1350 };
1351 let resource = TestResource {
1352 id: uuid::Uuid::new_v4(),
1353 };
1354 let result = checker
1355 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1356 .await;
1357
1358 match result {
1359 AccessEvaluation::Denied { reason, trace: _ } => {
1360 assert!(reason.contains("No policies configured"));
1361 }
1362 _ => panic!("Expected Denied(No policies configured), got {:?}", result),
1363 }
1364 }
1365
1366 #[tokio::test]
1367 async fn test_one_policy_allow() {
1368 let mut checker = PermissionChecker::new();
1369 checker.add_policy(AlwaysAllowPolicy);
1370
1371 let subject = TestSubject {
1372 id: uuid::Uuid::new_v4(),
1373 };
1374 let resource = TestResource {
1375 id: uuid::Uuid::new_v4(),
1376 };
1377
1378 let result = checker
1379 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1380 .await;
1381
1382 if let AccessEvaluation::Granted {
1383 policy_type,
1384 reason,
1385 trace,
1386 } = result
1387 {
1388 assert_eq!(policy_type, "AlwaysAllowPolicy");
1389 assert_eq!(reason, Some("Always allow policy".to_string()));
1390 let trace_str = trace.format();
1392 assert!(trace_str.contains("AlwaysAllowPolicy"));
1393 } else {
1394 panic!("Expected AccessEvaluation::Granted, got {:?}", result);
1395 }
1396 }
1397
1398 #[tokio::test]
1399 async fn test_one_policy_deny() {
1400 let mut checker = PermissionChecker::new();
1401 checker.add_policy(AlwaysDenyPolicy("DeniedByPolicy"));
1402
1403 let subject = TestSubject {
1404 id: uuid::Uuid::new_v4(),
1405 };
1406 let resource = TestResource {
1407 id: uuid::Uuid::new_v4(),
1408 };
1409
1410 let result = checker
1411 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1412 .await;
1413
1414 assert!(!result.is_granted());
1415 if let AccessEvaluation::Denied { reason, trace } = result {
1416 assert!(reason.contains("All policies denied access"));
1417 let trace_str = trace.format();
1418 assert!(trace_str.contains("DeniedByPolicy"));
1419 } else {
1420 panic!("Expected AccessEvaluation::Denied, got {:?}", result);
1421 }
1422 }
1423
1424 #[tokio::test]
1425 async fn test_multiple_policies_or_success() {
1426 let mut checker = PermissionChecker::new();
1428 checker.add_policy(AlwaysDenyPolicy("DenyPolicy"));
1429 checker.add_policy(AlwaysAllowPolicy);
1430
1431 let subject = TestSubject {
1432 id: uuid::Uuid::new_v4(),
1433 };
1434 let resource = TestResource {
1435 id: uuid::Uuid::new_v4(),
1436 };
1437 let result = checker
1438 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1439 .await;
1440 if let AccessEvaluation::Granted {
1441 policy_type,
1442 trace,
1443 reason: _,
1444 } = result
1445 {
1446 assert_eq!(policy_type, "AlwaysAllowPolicy");
1447 let trace_str = trace.format();
1448 assert!(trace_str.contains("DenyPolicy"));
1449 } else {
1450 panic!("Expected AccessEvaluation::Granted, got {:?}", result);
1451 }
1452 }
1453
1454 #[tokio::test]
1455 async fn test_multiple_policies_all_deny_collect_reasons() {
1456 let mut checker = PermissionChecker::new();
1458 checker.add_policy(AlwaysDenyPolicy("DenyPolicy1"));
1459 checker.add_policy(AlwaysDenyPolicy("DenyPolicy2"));
1460
1461 let subject = TestSubject {
1462 id: uuid::Uuid::new_v4(),
1463 };
1464 let resource = TestResource {
1465 id: uuid::Uuid::new_v4(),
1466 };
1467 let result = checker
1468 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1469 .await;
1470
1471 if let AccessEvaluation::Denied { trace, reason } = result {
1472 let trace_str = trace.format();
1473 assert!(trace_str.contains("DenyPolicy1"));
1474 assert!(trace_str.contains("DenyPolicy2"));
1475 assert_eq!(reason, "All policies denied access");
1476 } else {
1477 panic!("Expected AccessEvaluation::Denied, got {:?}", result);
1478 }
1479 }
1480
1481 pub struct DummyRelationshipResolver {
1487 relationships: Vec<(uuid::Uuid, uuid::Uuid, String)>,
1488 }
1489
1490 impl DummyRelationshipResolver {
1491 pub fn new(relationships: Vec<(uuid::Uuid, uuid::Uuid, String)>) -> Self {
1492 Self { relationships }
1493 }
1494 }
1495
1496 #[async_trait]
1497 impl RelationshipResolver<TestSubject, TestResource> for DummyRelationshipResolver {
1498 async fn has_relationship(
1499 &self,
1500 subject: &TestSubject,
1501 resource: &TestResource,
1502 relationship: &str,
1503 ) -> bool {
1504 self.relationships
1505 .iter()
1506 .any(|(s, r, rel)| s == &subject.id && r == &resource.id && rel == relationship)
1507 }
1508 }
1509
1510 #[tokio::test]
1511 async fn test_rebac_policy_allows_when_relationship_exists() {
1512 let subject_id = uuid::Uuid::new_v4();
1513 let resource_id = uuid::Uuid::new_v4();
1514 let relationship = "manager";
1515
1516 let subject = TestSubject { id: subject_id };
1517 let resource = TestResource { id: resource_id };
1518
1519 let resolver = DummyRelationshipResolver::new(vec![(
1521 subject_id,
1522 resource_id,
1523 relationship.to_string(),
1524 )]);
1525
1526 let policy = RebacPolicy::<TestSubject, TestResource, TestAction, TestContext, _>::new(
1527 relationship,
1528 resolver,
1529 );
1530
1531 let result = policy
1533 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1534 .await;
1535
1536 assert!(
1537 result.is_granted(),
1538 "Access should be allowed if relationship exists"
1539 );
1540 }
1541
1542 #[tokio::test]
1543 async fn test_rebac_policy_denies_when_relationship_missing() {
1544 let subject_id = uuid::Uuid::new_v4();
1545 let resource_id = uuid::Uuid::new_v4();
1546 let relationship = "manager";
1547
1548 let subject = TestSubject { id: subject_id };
1549 let resource = TestResource { id: resource_id };
1550
1551 let resolver = DummyRelationshipResolver::new(vec![]);
1553
1554 let policy = RebacPolicy::<TestSubject, TestResource, TestAction, TestContext, _>::new(
1555 relationship,
1556 resolver,
1557 );
1558
1559 let result = policy
1560 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1561 .await;
1562 assert!(
1564 !result.is_granted(),
1565 "Access should be denied if relationship does not exist"
1566 );
1567 }
1568
1569 #[tokio::test]
1571 async fn test_and_policy_allows_when_all_allow() {
1572 let policy = AndPolicy::try_new(vec![
1573 Arc::new(AlwaysAllowPolicy),
1574 Arc::new(AlwaysAllowPolicy),
1575 ])
1576 .expect("Unable to create and-policy policy");
1577 let subject = TestSubject {
1578 id: uuid::Uuid::new_v4(),
1579 };
1580 let resource = TestResource {
1581 id: uuid::Uuid::new_v4(),
1582 };
1583 let result = policy
1584 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1585 .await;
1586 assert!(
1587 result.is_granted(),
1588 "AndPolicy should allow access when all inner policies allow"
1589 );
1590 }
1591 #[tokio::test]
1592 async fn test_and_policy_denies_when_one_denies() {
1593 let policy = AndPolicy::try_new(vec![
1594 Arc::new(AlwaysAllowPolicy),
1595 Arc::new(AlwaysDenyPolicy("DenyInAnd")),
1596 ])
1597 .expect("Unable to create and-policy policy");
1598 let subject = TestSubject {
1599 id: uuid::Uuid::new_v4(),
1600 };
1601 let resource = TestResource {
1602 id: uuid::Uuid::new_v4(),
1603 };
1604 let result = policy
1605 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1606 .await;
1607 match result {
1608 PolicyEvalResult::Combined {
1609 policy_type,
1610 operation,
1611 children,
1612 outcome,
1613 } => {
1614 assert_eq!(operation, CombineOp::And);
1615 assert!(!outcome);
1616 assert_eq!(children.len(), 2);
1617 assert!(children[1].format(0).contains("DenyInAnd"));
1618 assert_eq!(policy_type, "AndPolicy");
1619 }
1620 _ => panic!("Expected Combined result from AndPolicy, got {:?}", result),
1621 }
1622 }
1623 #[tokio::test]
1624 async fn test_or_policy_allows_when_one_allows() {
1625 let policy = OrPolicy::try_new(vec![
1626 Arc::new(AlwaysDenyPolicy("Deny1")),
1627 Arc::new(AlwaysAllowPolicy),
1628 ])
1629 .expect("Unable to create or-policy policy");
1630 let subject = TestSubject {
1631 id: uuid::Uuid::new_v4(),
1632 };
1633 let resource = TestResource {
1634 id: uuid::Uuid::new_v4(),
1635 };
1636 let result = policy
1637 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1638 .await;
1639 assert!(
1640 result.is_granted(),
1641 "OrPolicy should allow access when at least one inner policy allows"
1642 );
1643 }
1644 #[tokio::test]
1645 async fn test_or_policy_denies_when_all_deny() {
1646 let policy = OrPolicy::try_new(vec![
1647 Arc::new(AlwaysDenyPolicy("Deny1")),
1648 Arc::new(AlwaysDenyPolicy("Deny2")),
1649 ])
1650 .expect("Unable to create or-policy policy");
1651 let subject = TestSubject {
1652 id: uuid::Uuid::new_v4(),
1653 };
1654 let resource = TestResource {
1655 id: uuid::Uuid::new_v4(),
1656 };
1657 let result = policy
1658 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1659 .await;
1660 match result {
1661 PolicyEvalResult::Combined {
1662 policy_type,
1663 operation,
1664 children,
1665 outcome,
1666 } => {
1667 assert_eq!(operation, CombineOp::Or);
1668 assert!(!outcome);
1669 assert_eq!(children.len(), 2);
1670 assert!(children[0].format(0).contains("Deny1"));
1671 assert!(children[1].format(0).contains("Deny2"));
1672 assert_eq!(policy_type, "OrPolicy");
1673 }
1674 _ => panic!("Expected Combined result from OrPolicy, got {:?}", result),
1675 }
1676 }
1677 #[tokio::test]
1678 async fn test_not_policy_allows_when_inner_denies() {
1679 let policy = NotPolicy::new(AlwaysDenyPolicy("AlwaysDeny"));
1680 let subject = TestSubject {
1681 id: uuid::Uuid::new_v4(),
1682 };
1683 let resource = TestResource {
1684 id: uuid::Uuid::new_v4(),
1685 };
1686 let result = policy
1687 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1688 .await;
1689 assert!(
1690 result.is_granted(),
1691 "NotPolicy should allow access when inner policy denies"
1692 );
1693 }
1694 #[tokio::test]
1695 async fn test_not_policy_denies_when_inner_allows() {
1696 let policy = NotPolicy::new(AlwaysAllowPolicy);
1697 let subject = TestSubject {
1698 id: uuid::Uuid::new_v4(),
1699 };
1700 let resource = TestResource {
1701 id: uuid::Uuid::new_v4(),
1702 };
1703 let result = policy
1704 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1705 .await;
1706 match result {
1707 PolicyEvalResult::Combined {
1708 policy_type,
1709 operation,
1710 children,
1711 outcome,
1712 } => {
1713 assert_eq!(operation, CombineOp::Not);
1714 assert!(!outcome);
1715 assert_eq!(children.len(), 1);
1716 assert!(children[0].format(0).contains("AlwaysAllowPolicy"));
1717 assert_eq!(policy_type, "NotPolicy");
1718 }
1719 _ => panic!("Expected Combined result from NotPolicy, got {:?}", result),
1720 }
1721 }
1722
1723 #[tokio::test]
1724 async fn test_empty_policies_in_combinators() {
1725 let and_policy_result =
1727 AndPolicy::<TestSubject, TestResource, TestAction, TestContext>::try_new(vec![]);
1728
1729 assert!(and_policy_result.is_err());
1730
1731 let or_policy_result =
1733 OrPolicy::<TestSubject, TestResource, TestAction, TestContext>::try_new(vec![]);
1734 assert!(or_policy_result.is_err());
1735 }
1736
1737 #[tokio::test]
1738 async fn test_deeply_nested_combinators() {
1739 let inner_not = NotPolicy::new(AlwaysDenyPolicy("InnerDeny"));
1741
1742 let inner_or = OrPolicy::try_new(vec![
1743 Arc::new(AlwaysDenyPolicy("MidDeny")),
1744 Arc::new(inner_not),
1745 ])
1746 .expect("Unable to create or-policy policy");
1747
1748 let inner_and = AndPolicy::try_new(vec![Arc::new(AlwaysAllowPolicy), Arc::new(inner_or)])
1749 .expect("Unable to create and-policy policy");
1750
1751 let outer_not = NotPolicy::new(inner_and);
1752
1753 let subject = TestSubject {
1754 id: uuid::Uuid::new_v4(),
1755 };
1756 let resource = TestResource {
1757 id: uuid::Uuid::new_v4(),
1758 };
1759
1760 let result = outer_not
1761 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1762 .await;
1763
1764 assert!(!result.is_granted());
1766
1767 let trace_str = result.format(0);
1769 assert!(trace_str.contains("NOT"));
1770 assert!(trace_str.contains("AND"));
1771 assert!(trace_str.contains("OR"));
1772 assert!(trace_str.contains("InnerDeny"));
1773 }
1774
1775 #[derive(Debug, Clone)]
1776 struct FeatureFlagContext {
1777 feature_enabled: bool,
1778 }
1779
1780 struct FeatureFlagPolicy;
1781
1782 #[async_trait]
1783 impl Policy<TestSubject, TestResource, TestAction, FeatureFlagContext> for FeatureFlagPolicy {
1784 async fn evaluate_access(
1785 &self,
1786 _subject: &TestSubject,
1787 _action: &TestAction,
1788 _resource: &TestResource,
1789 context: &FeatureFlagContext,
1790 ) -> PolicyEvalResult {
1791 if context.feature_enabled {
1792 PolicyEvalResult::Granted {
1793 policy_type: self.policy_type(),
1794 reason: Some("Feature flag enabled".to_string()),
1795 }
1796 } else {
1797 PolicyEvalResult::Denied {
1798 policy_type: self.policy_type(),
1799 reason: "Feature flag disabled".to_string(),
1800 }
1801 }
1802 }
1803
1804 fn policy_type(&self) -> String {
1805 "FeatureFlagPolicy".to_string()
1806 }
1807 }
1808
1809 #[tokio::test]
1810 async fn test_context_sensitive_policy() {
1811 let policy = FeatureFlagPolicy;
1812 let subject = TestSubject {
1813 id: uuid::Uuid::new_v4(),
1814 };
1815 let resource = TestResource {
1816 id: uuid::Uuid::new_v4(),
1817 };
1818
1819 let context_enabled = FeatureFlagContext {
1821 feature_enabled: true,
1822 };
1823 let result = policy
1824 .evaluate_access(&subject, &TestAction, &resource, &context_enabled)
1825 .await;
1826 assert!(result.is_granted());
1827
1828 let context_disabled = FeatureFlagContext {
1830 feature_enabled: false,
1831 };
1832 let result = policy
1833 .evaluate_access(&subject, &TestAction, &resource, &context_disabled)
1834 .await;
1835 assert!(!result.is_granted());
1836 }
1837
1838 #[tokio::test]
1839 async fn test_short_circuit_evaluation() {
1840 use std::sync::atomic::{AtomicUsize, Ordering};
1842 use std::sync::Arc as StdArc;
1843
1844 let evaluation_count = StdArc::new(AtomicUsize::new(0));
1845
1846 struct CountingPolicy {
1847 result: bool,
1848 counter: StdArc<AtomicUsize>,
1849 }
1850
1851 #[async_trait]
1852 impl Policy<TestSubject, TestResource, TestAction, TestContext> for CountingPolicy {
1853 async fn evaluate_access(
1854 &self,
1855 _subject: &TestSubject,
1856 _action: &TestAction,
1857 _resource: &TestResource,
1858 _context: &TestContext,
1859 ) -> PolicyEvalResult {
1860 self.counter.fetch_add(1, Ordering::SeqCst);
1861
1862 if self.result {
1863 PolicyEvalResult::Granted {
1864 policy_type: self.policy_type(),
1865 reason: Some("Counting policy granted".to_string()),
1866 }
1867 } else {
1868 PolicyEvalResult::Denied {
1869 policy_type: self.policy_type(),
1870 reason: "Counting policy denied".to_string(),
1871 }
1872 }
1873 }
1874
1875 fn policy_type(&self) -> String {
1876 "CountingPolicy".to_string()
1877 }
1878 }
1879
1880 let count_clone = evaluation_count.clone();
1882 evaluation_count.store(0, Ordering::SeqCst);
1883
1884 let and_policy = AndPolicy::try_new(vec![
1885 Arc::new(CountingPolicy {
1886 result: false,
1887 counter: count_clone.clone(),
1888 }),
1889 Arc::new(CountingPolicy {
1890 result: true,
1891 counter: count_clone,
1892 }),
1893 ])
1894 .expect("Unable to create 'and' policy");
1895
1896 let subject = TestSubject {
1897 id: uuid::Uuid::new_v4(),
1898 };
1899 let resource = TestResource {
1900 id: uuid::Uuid::new_v4(),
1901 };
1902 and_policy
1903 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1904 .await;
1905
1906 assert_eq!(
1907 evaluation_count.load(Ordering::SeqCst),
1908 1,
1909 "AND policy should short-circuit after first deny"
1910 );
1911
1912 let count_clone = evaluation_count.clone();
1914 evaluation_count.store(0, Ordering::SeqCst);
1915
1916 let or_policy = OrPolicy::try_new(vec![
1917 Arc::new(CountingPolicy {
1918 result: true,
1919 counter: count_clone.clone(),
1920 }),
1921 Arc::new(CountingPolicy {
1922 result: false,
1923 counter: count_clone,
1924 }),
1925 ])
1926 .unwrap();
1927
1928 or_policy
1929 .evaluate_access(&subject, &TestAction, &resource, &TestContext)
1930 .await;
1931
1932 assert_eq!(
1933 evaluation_count.load(Ordering::SeqCst),
1934 1,
1935 "OR policy should short-circuit after first allow"
1936 );
1937 }
1938}
1939
1940#[cfg(test)]
1941mod policy_builder_tests {
1942 use super::*;
1943 use uuid::Uuid;
1944
1945 #[derive(Debug, Clone)]
1947 struct TestSubject {
1948 pub name: String,
1949 }
1950 #[derive(Debug, Clone)]
1951 struct TestAction;
1952 #[derive(Debug, Clone)]
1953 struct TestResource;
1954 #[derive(Debug, Clone)]
1955 struct TestContext;
1956
1957 #[tokio::test]
1959 async fn test_policy_builder_allows_when_no_predicates() {
1960 let policy = PolicyBuilder::<TestSubject, TestResource, TestAction, TestContext>::new(
1961 "NoPredicatesPolicy",
1962 )
1963 .build();
1964
1965 let result = policy
1966 .evaluate_access(
1967 &TestSubject { name: "Any".into() },
1968 &TestAction,
1969 &TestResource,
1970 &TestContext,
1971 )
1972 .await;
1973 assert!(
1974 result.is_granted(),
1975 "Policy built with no predicates should allow access (default true)"
1976 );
1977 }
1978
1979 #[tokio::test]
1981 async fn test_policy_builder_with_subject_predicate() {
1982 let policy = PolicyBuilder::<TestSubject, TestResource, TestAction, TestContext>::new(
1983 "SubjectPolicy",
1984 )
1985 .subjects(|s: &TestSubject| s.name == "Alice")
1986 .build();
1987
1988 let result1 = policy
1990 .evaluate_access(
1991 &TestSubject {
1992 name: "Alice".into(),
1993 },
1994 &TestAction,
1995 &TestResource,
1996 &TestContext,
1997 )
1998 .await;
1999 assert!(
2000 result1.is_granted(),
2001 "Policy should allow access for subject 'Alice'"
2002 );
2003
2004 let result2 = policy
2006 .evaluate_access(
2007 &TestSubject { name: "Bob".into() },
2008 &TestAction,
2009 &TestResource,
2010 &TestContext,
2011 )
2012 .await;
2013 assert!(
2014 !result2.is_granted(),
2015 "Policy should deny access for subject not named 'Alice'"
2016 );
2017 }
2018
2019 #[tokio::test]
2021 async fn test_policy_builder_effect_deny() {
2022 let policy =
2023 PolicyBuilder::<TestSubject, TestResource, TestAction, TestContext>::new("DenyPolicy")
2024 .effect(Effect::Deny)
2025 .build();
2026
2027 let result = policy
2030 .evaluate_access(
2031 &TestSubject {
2032 name: "Anyone".into(),
2033 },
2034 &TestAction,
2035 &TestResource,
2036 &TestContext,
2037 )
2038 .await;
2039 assert!(
2040 !result.is_granted(),
2041 "Policy with effect Deny should result in denial even if the predicate passes"
2042 );
2043 }
2044
2045 #[tokio::test]
2047 async fn test_policy_builder_with_extra_condition() {
2048 #[derive(Debug, Clone)]
2049 struct ExtendedSubject {
2050 pub id: Uuid,
2051 pub name: String,
2052 }
2053 #[derive(Debug, Clone)]
2054 struct ExtendedResource {
2055 pub owner_id: Uuid,
2056 }
2057 #[derive(Debug, Clone)]
2058 struct ExtendedAction;
2059 #[derive(Debug, Clone)]
2060 struct ExtendedContext;
2061
2062 let subject_id = Uuid::new_v4();
2066 let policy = PolicyBuilder::<
2067 ExtendedSubject,
2068 ExtendedResource,
2069 ExtendedAction,
2070 ExtendedContext,
2071 >::new("AliceOwnerPolicy")
2072 .subjects(|s: &ExtendedSubject| s.name == "Alice")
2073 .when(|s, _a, r, _c| s.id == r.owner_id)
2074 .build();
2075
2076 let result1 = policy
2078 .evaluate_access(
2079 &ExtendedSubject {
2080 id: subject_id,
2081 name: "Alice".into(),
2082 },
2083 &ExtendedAction,
2084 &ExtendedResource {
2085 owner_id: subject_id,
2086 },
2087 &ExtendedContext,
2088 )
2089 .await;
2090 assert!(
2091 result1.is_granted(),
2092 "Policy should allow access when conditions are met"
2093 );
2094
2095 let result2 = policy
2097 .evaluate_access(
2098 &ExtendedSubject {
2099 id: subject_id,
2100 name: "Alice".into(),
2101 },
2102 &ExtendedAction,
2103 &ExtendedResource {
2104 owner_id: Uuid::new_v4(),
2105 },
2106 &ExtendedContext,
2107 )
2108 .await;
2109 assert!(
2110 !result2.is_granted(),
2111 "Policy should deny access when extra condition fails"
2112 );
2113 }
2114}