gatehouse/
lib.rs

1//! A flexible authorization library that combines role‐based (RBAC),
2//! attribute‐based (ABAC), and relationship‐based (ReBAC) policies.
3//! The library provides a generic `Policy` trait for defining custom policies,
4//! a builder pattern for creating custom policies as well as several built-in
5//! policies for common use cases, and combinators (AndPolicy, OrPolicy, NotPolicy) for
6//! composing complex authorization logic.
7//!
8//! # Overview
9//!
10//! A *Policy* is an asynchronous decision unit that checks if a given subject may
11//! perform an action on a resource within a given context. Policies implement the
12//! [`Policy`] trait. A [`PermissionChecker`] aggregates multiple policies and uses OR
13//! logic by default (i.e. if any policy grants access, then access is allowed).
14//!
15//! ## Policies
16//!
17//! Below we define a simple system where a user may read a document if they
18//! are an admin (via a simple role-based policy) or if they are the owner of the document (via
19//! an attribute-based policy).
20//!
21//! ```rust
22//! # use uuid::Uuid;
23//! # use async_trait::async_trait;
24//! # use std::sync::Arc;
25//! # use gatehouse::*;
26//!
27//! // Define our core types.
28//! #[derive(Debug, Clone)]
29//! pub struct User {
30//!     pub id: Uuid,
31//!     pub roles: Vec<String>,
32//! }
33//!
34//! #[derive(Debug, Clone)]
35//! pub struct Document {
36//!     pub id: Uuid,
37//!     pub owner_id: Uuid,
38//! }
39//!
40//! #[derive(Debug, Clone)]
41//! pub struct ReadAction;
42//!
43//! #[derive(Debug, Clone)]
44//! pub struct EmptyContext;
45//!
46//! // A simple RBAC policy: grant access if the user has the "admin" role.
47//! struct AdminPolicy;
48//! #[async_trait]
49//! impl Policy<User, Document, ReadAction, EmptyContext> for AdminPolicy {
50//!     async fn evaluate_access(
51//!         &self,
52//!         user: &User,
53//!         _action: &ReadAction,
54//!         _resource: &Document,
55//!         _context: &EmptyContext,
56//!     ) -> PolicyEvalResult {
57//!         if user.roles.contains(&"admin".to_string()) {
58//!             PolicyEvalResult::Granted {
59//!                 policy_type: self.policy_type(),
60//!                 reason: Some("User is admin".to_string()),
61//!             }
62//!         } else {
63//!             PolicyEvalResult::Denied {
64//!                 policy_type: self.policy_type(),
65//!                 reason: "User is not admin".to_string(),
66//!             }
67//!         }
68//!     }
69//!     fn policy_type(&self) -> String { "AdminPolicy".to_string() }
70//! }
71//!
72//! // An ABAC policy: grant access if the user is the owner of the document.
73//! struct OwnerPolicy;
74//!
75//! #[async_trait]
76//! impl Policy<User, Document, ReadAction, EmptyContext> for OwnerPolicy {
77//!     async fn evaluate_access(
78//!         &self,
79//!         user: &User,
80//!         _action: &ReadAction,
81//!         document: &Document,
82//!         _context: &EmptyContext,
83//!     ) -> PolicyEvalResult {
84//!         if user.id == document.owner_id {
85//!             PolicyEvalResult::Granted {
86//!                 policy_type: self.policy_type(),
87//!                 reason: Some("User is the owner".to_string()),
88//!             }
89//!         } else {
90//!             PolicyEvalResult::Denied {
91//!                policy_type: self.policy_type(),
92//!                reason: "User is not the owner".to_string(),
93//!            }
94//!         }
95//!     }
96//!     fn policy_type(&self) -> String {
97//!         "OwnerPolicy".to_string()
98//!     }
99//! }
100//!
101//! // Create a PermissionChecker (which uses OR semantics by default) and add both policies.
102//! fn create_document_checker() -> PermissionChecker<User, Document, ReadAction, EmptyContext> {
103//!     let mut checker = PermissionChecker::new();
104//!     checker.add_policy(AdminPolicy);
105//!     checker.add_policy(OwnerPolicy);
106//!     checker
107//! }
108//!
109//! # tokio_test::block_on(async {
110//! let admin_user = User {
111//!     id: Uuid::new_v4(),
112//!     roles: vec!["admin".into()],
113//! };
114//!
115//! let owner_user = User {
116//!     id: Uuid::new_v4(),
117//!     roles: vec!["user".into()],
118//! };
119//!
120//! let document = Document {
121//!     id: Uuid::new_v4(),
122//!     owner_id: owner_user.id,
123//! };
124//!
125//! let checker = create_document_checker();
126//!
127//! // An admin should have access.
128//! assert!(checker.evaluate_access(&admin_user, &ReadAction, &document, &EmptyContext).await.is_granted());
129//!
130//! // The owner should have access.
131//! assert!(checker.evaluate_access(&owner_user, &ReadAction, &document, &EmptyContext).await.is_granted());
132//!
133//! // A random user should be denied access.
134//! let random_user = User {
135//!     id: Uuid::new_v4(),
136//!     roles: vec!["user".into()],
137//! };
138//! assert!(!checker.evaluate_access(&random_user, &ReadAction, &document, &EmptyContext).await.is_granted());
139//! # });
140//! ```
141//!
142//! # Evaluation Tracing
143//!
144//! The permission system provides detailed tracing of policy decisions, see [`AccessEvaluation`]\
145//! for an example.
146//!
147//!
148//! ## Combinators
149//!
150//! Sometimes you may want to require that several policies pass (AND), require that
151//! at least one passes (OR), or even invert a policy (NOT). `permissions` provides:
152//!
153//! - [`AndPolicy`]: Grants access only if all inner policies allow access. Otherwise,
154//!   returns a combined error.
155//! - [`OrPolicy`]: Grants access if any inner policy allows access; otherwise returns a
156//!   combined error.
157//! - [`NotPolicy`]: Inverts the decision of an inner policy.
158//!
159//!
160//! ## Built in Policies
161//! The library provides a few built-in policies:
162//!  - [`RbacPolicy`]: A role-based access control policy.
163//!  - [`AbacPolicy`]: An attribute-based access control policy.
164//!  - [`RebacPolicy`]: A relationship-based access control policy.
165//!
166#![allow(clippy::type_complexity)]
167use async_trait::async_trait;
168use std::fmt;
169use std::sync::Arc;
170
171/// The type of boolean combining operation a policy might represent.
172#[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/// The result of evaluating a single policy (or a combination).
190///
191/// This enum is used both by individual policies and by combinators to represent the
192/// outcome of access evaluation.
193///
194/// - [`Granted`]: Indicates that access is granted, with an optional reason.
195/// - [`Denied`]: Indicates that access is denied, along with an explanatory reason.
196/// - [`Combined`]: Represents the aggregate result of combining multiple policies.
197
198#[derive(Debug, Clone)]
199pub enum PolicyEvalResult {
200    /// Access granted. Contains the policy type and an optional reason.
201    Granted {
202        policy_type: String,
203        reason: Option<String>,
204    },
205    /// Access denied. Contains the policy type and a reason.
206    Denied { policy_type: String, reason: String },
207    /// Combined result from multiple policy evaluations.
208    /// Contains the policy type, the combining operation (e.g. "AND", "OR", "NOT"),
209    /// a list of child evaluation results, and the overall outcome.
210    Combined {
211        policy_type: String,
212        operation: CombineOp,
213        children: Vec<PolicyEvalResult>,
214        outcome: bool,
215    },
216}
217
218/// The complete result of a permission evaluation.
219/// Contains both the final decision and a detailed trace for debugging.
220///
221/// ### Evaluation Tracing
222///
223/// The permission system provides detailed tracing of policy decisions:
224/// ```rust
225/// # use gatehouse::*;
226/// # use uuid::Uuid;
227/// #
228/// # // Define simple types for the example
229/// # #[derive(Debug, Clone)]
230/// # struct User { id: Uuid }
231/// # #[derive(Debug, Clone)]
232/// # struct Document { id: Uuid }
233/// # #[derive(Debug, Clone)]
234/// # struct ReadAction;
235/// # #[derive(Debug, Clone)]
236/// # struct EmptyContext;
237/// #
238/// # async fn example() -> AccessEvaluation {
239/// #     let mut checker = PermissionChecker::<User, Document, ReadAction, EmptyContext>::new();
240/// #     let user = User { id: Uuid::new_v4() };
241/// #     let document = Document { id: Uuid::new_v4() };
242/// #     checker.evaluate_access(&user, &ReadAction, &document, &EmptyContext).await
243/// # }
244/// #
245/// # tokio_test::block_on(async {
246/// let result = example().await;
247///
248/// match result {
249///     AccessEvaluation::Granted { policy_type, reason, trace } => {
250///         println!("Access granted by {}: {:?}", policy_type, reason);
251///         println!("Full evaluation trace:\n{}", trace.format());
252///     }
253///     AccessEvaluation::Denied { reason, trace } => {
254///         println!("Access denied: {}", reason);
255///         println!("Full evaluation trace:\n{}", trace.format());
256///     }
257/// }
258/// # });
259/// ```
260#[derive(Debug, Clone)]
261pub enum AccessEvaluation {
262    /// Access was granted.
263    Granted {
264        /// The policy that granted access
265        policy_type: String,
266        /// Optional reason for granting
267        reason: Option<String>,
268        /// Full evaluation trace including any rejected policies
269        trace: EvalTrace,
270    },
271    /// Access was denied.
272    Denied {
273        /// The complete evaluation trace showing all policy decisions
274        trace: EvalTrace,
275        /// Summary reason for denial
276        reason: String,
277    },
278}
279
280impl AccessEvaluation {
281    /// Whether access was granted
282    pub fn is_granted(&self) -> bool {
283        matches!(self, Self::Granted { .. })
284    }
285
286    /// Converts the evaluation into a `Result`, mapping a denial into an error.
287    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        // If there's an actual tree to show, add it. Otherwise, fallback.
305        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
314/// A concise line about the final decision.
315impl 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                // Headline
324                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/// Container for the evaluation tree
337/// Detailed trace of all policy evaluations
338#[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    /// Returns a formatted representation of the evaluation tree
361    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    /// Returns whether this evaluation resulted in access being granted
371    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    /// Returns the reason string if available
380    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    /// Formats the evaluation tree with indentation for readability
389    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/// A generic async trait representing a single authorization policy.
437/// A policy determines if a subject is allowed to perform an action on
438/// a resource within a given context.
439#[async_trait]
440pub trait Policy<Subject, Resource, Action, Context>: Send + Sync {
441    /// Evaluates whether access should be granted.
442    ///
443    /// # Arguments
444    ///
445    /// * `subject` - The entity requesting access.
446    /// * `action` - The action being performed.
447    /// * `resource` - The target resource.
448    /// * `context` - Additional context that may affect the decision.
449    ///
450    /// # Returns
451    ///
452    /// A [`PolicyEvalResult`] indicating whether access is granted or denied.
453    async fn evaluate_access(
454        &self,
455        subject: &Subject,
456        action: &Action,
457        resource: &Resource,
458        context: &Context,
459    ) -> PolicyEvalResult;
460
461    /// Policy name for debugging
462    fn policy_type(&self) -> String;
463}
464
465/// A container for multiple policies, applied in an "OR" fashion.
466/// (If any policy returns Ok, access is granted)
467/// **Important**:
468/// If no policies are added, access is always denied.
469#[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    /// Creates a new `PermissionChecker` with no policies.
482    pub fn new() -> Self {
483        Self {
484            policies: Vec::new(),
485        }
486    }
487
488    /// Adds a policy to the checker.
489    ///
490    /// # Arguments
491    ///
492    /// * `policy` - A type implementing [`Policy`]. It is stored as an `Arc` for shared ownership.
493    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    /// Evaluates all policies against the given parameters.
498    ///
499    /// Policies are evaluated sequentially with OR semantics (short-circuiting on first success).
500    /// Returns an [`AccessEvaluation`] with detailed tracing.
501    #[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        // Evaluate each policy
526        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 any policy allows access, return immediately
534            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        // If all policies denied access
551        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/// Represents the intended effect of a policy.
567///
568/// `Allow` means the policy grants access; `Deny` means it denies access.
569#[derive(Debug, Clone, PartialEq, Eq)]
570pub enum Effect {
571    Allow,
572    Deny,
573}
574
575/// An internal policy type (not exposed to API users) that is constructed via the builder.
576struct InternalPolicy<S, R, A, C> {
577    name: String,
578    effect: Effect,
579    // The predicate returns true if all conditions pass.
580    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            // Predicate didn't match – treat as non-applicable (denied).
611            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// Tell the compiler that a Box<dyn Policy> implements the Policy trait so we can keep
623// our internal policy type private.
624#[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
649/// A builder API for creating custom policies.
650///
651/// The [`PolicyBuilder`] offers a fluent interface to combine predicate functions
652/// on the subject, action, resource, and context. Use it to construct a policy that
653/// can be added to a [`PermissionChecker`].
654pub 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    // Note the order here matches the evaluate_access signature
668    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    /// Creates a new policy builder with the given name.
679    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    /// Sets the effect (Allow or Deny) for the policy.
692    /// Defaults to Allow
693    pub fn effect(mut self, effect: Effect) -> Self {
694        self.effect = effect;
695        self
696    }
697
698    /// Adds a predicate that tests the subject.
699    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    /// Adds a predicate that tests the action.
708    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    /// Adds a predicate that tests the resource.
717    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    /// Add a predicate that validates the context.
726    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    /// Add a condition that considers all four inputs.
735    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    /// Build the policy. Returns a boxed policy that can be added to a PermissionChecker.
744    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
768/// A role-based access control policy.
769///
770/// `required_roles_resolver` is a closure that determines which roles are required
771/// for the given (resource, action). `user_roles_resolver` extracts the subject's roles.
772pub 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
827/// An attribute-based access control policy.
828/// Define a `condition` closure that determines whether a subject is allowed to
829/// perform an action on a resource, given the additional context. If it returns
830/// true, access is granted. Otherwise, access is denied.
831///
832/// ## Example
833///
834/// We define simple types for a user, a resource, an action, and a context.
835/// We then create a built-in ABAC policy that grants access if the user "owns"
836/// a resource as determined by the resource's owner_id.
837///
838/// ```rust
839/// # use async_trait::async_trait;
840/// # use std::sync::Arc;
841/// # use uuid::Uuid;
842/// # use gatehouse::*;
843///
844/// // Define our core types.
845/// #[derive(Debug, Clone)]
846/// struct User {
847///     id: Uuid,
848/// }
849///
850/// #[derive(Debug, Clone)]
851/// struct Resource {
852///     owner_id: Uuid,
853/// }
854///
855/// #[derive(Debug, Clone)]
856/// struct Action;
857///
858/// #[derive(Debug, Clone)]
859/// struct EmptyContext;
860///
861/// // Create an ABAC policy.
862/// // This policy grants access if the user's ID matches the resource's owner.
863/// let abac_policy = AbacPolicy::new(
864///     |user: &User, resource: &Resource, _action: &Action, _context: &EmptyContext| {
865///         user.id == resource.owner_id
866///     },
867/// );
868///
869/// // Create a PermissionChecker and add the ABAC policy.
870/// let mut checker = PermissionChecker::<User, Resource, Action, EmptyContext>::new();
871/// checker.add_policy(abac_policy);
872///
873/// // Create a sample user
874/// let user = User {
875///     id: Uuid::new_v4(),
876/// };
877///
878/// // Create a resource owned by the user, and one that is not
879/// let owned_resource = Resource { owner_id: user.id };
880/// let other_resource = Resource { owner_id: Uuid::new_v4() };
881/// let context = EmptyContext;
882///
883/// # tokio_test::block_on(async {
884/// // This check should succeed because the user is the owner:
885/// assert!(checker.evaluate_access(&user, &Action, &owned_resource, &context).await.is_granted());
886///
887/// // This check should fail because the user is not the owner:
888/// assert!(checker.evaluate_access(&user, &Action, &other_resource, &context).await.is_granted() == false);
889/// # });
890/// ```
891///
892pub 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/// A trait that abstracts a relationship resolver.
943/// Given a subject and a resource, the resolver answers whether the
944/// specified relationship e.g. "creator", "manager" exists between them.
945#[async_trait]
946pub trait RelationshipResolver<S, R>: Send + Sync {
947    async fn has_relationship(&self, subject: &S, resource: &R, relationship: &str) -> bool;
948}
949
950/// ### ReBAC Policy
951///
952/// In this example, we show how to use a built-in relationship-based (ReBAC) policy. We define
953/// a dummy relationship resolver that checks if a user is the manager of a project.
954///
955/// ```rust
956/// use async_trait::async_trait;
957/// use std::sync::Arc;
958/// use uuid::Uuid;
959/// use gatehouse::*;
960///
961/// #[derive(Debug, Clone)]
962/// pub struct Employee {
963///     pub id: Uuid,
964/// }
965///
966/// #[derive(Debug, Clone)]
967/// pub struct Project {
968///     pub id: Uuid,
969///     pub manager_id: Uuid,
970/// }
971///
972/// #[derive(Debug, Clone)]
973/// pub struct AccessAction;
974///
975/// #[derive(Debug, Clone)]
976/// pub struct EmptyContext;
977///
978/// // Define a dummy relationship resolver that considers an employee to be a manager
979/// // of a project if their id matches the project's manager_id.
980/// struct DummyRelationshipResolver;
981///
982/// #[async_trait]
983/// impl RelationshipResolver<Employee, Project> for DummyRelationshipResolver {
984///     async fn has_relationship(
985///         &self,
986///         employee: &Employee,
987///         project: &Project,
988///         relationship: &str,
989///     ) -> bool {
990///         relationship == "manager" && employee.id == project.manager_id
991///     }
992/// }
993///
994/// // Create a ReBAC policy that checks for the "manager" relationship.
995/// let rebac_policy = RebacPolicy::<Employee, Project, AccessAction, EmptyContext, _>::new(
996///     "manager",
997///     DummyRelationshipResolver,
998/// );
999///
1000/// // Create a PermissionChecker and add the ReBAC policy.
1001/// let mut checker = PermissionChecker::<Employee, Project, AccessAction, EmptyContext>::new();
1002/// checker.add_policy(rebac_policy);
1003///
1004/// // Create a sample employee and project.
1005/// let manager = Employee { id: Uuid::new_v4() };
1006/// let project = Project {
1007///     id: Uuid::new_v4(),
1008///     manager_id: manager.id,
1009/// };
1010/// let context = EmptyContext;
1011///
1012/// // The manager should have access.
1013/// # tokio_test::block_on(async {
1014/// assert!(checker.evaluate_access(&manager, &AccessAction, &project, &context).await.is_granted());
1015///
1016/// // A different employee should be denied access.
1017/// let other_employee = Employee { id: Uuid::new_v4() };
1018/// assert!(!checker.evaluate_access(&other_employee, &AccessAction, &project, &context).await.is_granted());
1019/// # });
1020/// ```
1021pub 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    /// Create a new RebacPolicy for a given relationship string.
1029    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
1083/// ---
1084/// Policy Combinators
1085/// ---
1086///
1087/// AndPolicy
1088///
1089/// Combines multiple policies with a logical AND. Access is granted only if every
1090/// inner policy grants access.
1091pub struct AndPolicy<S, R, A, C> {
1092    policies: Vec<Arc<dyn Policy<S, R, A, C>>>,
1093}
1094
1095/// Error returned when no policies are provided to a combinator policy.
1096#[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    // Override the default policy_type implementation
1120    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            // Short-circuit on first denial
1140            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        // All policies granted access
1151        PolicyEvalResult::Combined {
1152            policy_type: self.policy_type(),
1153            operation: CombineOp::And,
1154            children: children_results,
1155            outcome: true,
1156        }
1157    }
1158}
1159
1160/// OrPolicy
1161///
1162/// Combines multiple policies with a logical OR. Access is granted if any inner policy
1163/// grants access.
1164pub 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    // Override the default policy_type implementation
1187    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            // Short-circuit on first success
1206            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        // All policies denied access
1217        PolicyEvalResult::Combined {
1218            policy_type: self.policy_type(),
1219            operation: CombineOp::Or,
1220            children: children_results,
1221            outcome: false,
1222        }
1223    }
1224}
1225
1226/// NotPolicy
1227///
1228/// Inverts the result of an inner policy. If the inner policy allows access, then NotPolicy
1229/// denies it, and vice versa.
1230pub 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    // Override the default policy_type implementation
1251    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    // Dummy resource/action/context types for testing
1281    #[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    // A policy that always allows
1298    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    // A policy that always denies, with a custom reason
1321    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            // Check the trace to ensure the policy was evaluated
1391            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        // First policy denies, second allows. Checker should return Ok, short-circuiting on second.
1427        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        // Both policies deny, so we expect a Forbidden
1457        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    // RebacPolicy tests with a dummy resolver.
1482
1483    /// In-memory relationship resolver for testing.
1484    /// It holds a vector of tuples (subject_id, resource_id, relationship)
1485    /// to represent existing relationships.
1486    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        // Create a dummy resolver that knows the subject is a manager of the resource.
1520        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        // Action and context are not used by RebacPolicy, so we pass dummy values.
1532        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        // Create a dummy resolver with no relationships.
1552        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        // Check access is denied
1563        assert!(
1564            !result.is_granted(),
1565            "Access should be denied if relationship does not exist"
1566        );
1567    }
1568
1569    // Combinator tests.
1570    #[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        // Test AndPolicy with no policies
1726        let and_policy_result =
1727            AndPolicy::<TestSubject, TestResource, TestAction, TestContext>::try_new(vec![]);
1728
1729        assert!(and_policy_result.is_err());
1730
1731        // Test OrPolicy with no policies
1732        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        // Create a complex policy structure: NOT(AND(Allow, OR(Deny, NOT(Deny))))
1740        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        // This complex structure should result in a denial
1765        assert!(!result.is_granted());
1766
1767        // Verify the correct structure of the trace
1768        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        // Test with flag enabled
1820        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        // Test with flag disabled
1829        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        // Create a counter to track policy evaluation
1841        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        // Test AND short circuit on first deny
1881        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        // Test OR short circuit on first allow
1913        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    // Define simple test types
1946    #[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    // Test that with no predicates the builder returns a policy that always "matches"
1958    #[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    // Test that a subject predicate is applied correctly.
1980    #[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        // Should allow if the subject's name is "Alice"
1989        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        // Otherwise, it should deny
2005        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    // Test that setting the effect to Deny overrides an otherwise matching predicate.
2020    #[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        // Even though no predicate fails (so predicate returns true),
2028        // the effect should result in a Denied outcome.
2029        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    // Test that extra conditions (combining multiple inputs) work correctly.
2046    #[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        // Build a policy that checks:
2063        //   1. Subject's name is "Alice"
2064        //   2. And that subject.id == resource.owner_id (via extra condition)
2065        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        // Case where both conditions are met.
2077        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        // Case where extra condition fails (different id)
2096        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}