Skip to main content

gatekeep/
policy.rs

1use crate::{
2    ClauseLabel, Condition, DenyShape, GatekeepResult, ObligationId, ObligationSpec, Policy,
3    ReasonCode,
4};
5
6/// Builds an unconditional permit policy.
7#[must_use]
8pub const fn permit<O>(outcome: O) -> Policy<O> {
9    Policy::Permit(outcome)
10}
11
12/// Builds an unconditional denial policy.
13#[must_use]
14pub const fn deny<O>() -> Policy<O> {
15    Policy::Deny
16}
17
18/// Builds a conditional denial guard.
19///
20/// The returned policy permits when `condition` is false and denies with
21/// `reason` when `condition` is true. Place these guards in `policy::all` before
22/// positive grant clauses to get ordered fail-closed precedence.
23#[must_use]
24pub fn deny_when<O: crate::Lattice>(
25    condition: Condition,
26    reason: impl IntoReasonCode,
27) -> Policy<O> {
28    grant(O::top(), crate::condition::not(condition)).reason(reason)
29}
30
31/// Builds a conditional grant policy.
32#[must_use]
33pub const fn grant<O>(outcome: O, condition: Condition) -> Policy<O> {
34    Policy::Grant {
35        outcome,
36        condition,
37        label: None,
38        deny_shape: DenyShape::Forbidden,
39        obligations: Vec::new(),
40        reason: None,
41    }
42}
43
44/// Builds a grant-only policy builder.
45///
46/// Unlike [`Policy::labeled`], [`Policy::hidden`], [`Policy::reason`], and
47/// [`Policy::with_obligation`], these methods are only available on grant
48/// clauses and cannot silently no-op on other policy variants.
49#[must_use]
50pub const fn grant_clause<O>(outcome: O, condition: Condition) -> GrantPolicy<O> {
51    GrantPolicy {
52        outcome,
53        condition,
54        label: None,
55        deny_shape: DenyShape::Forbidden,
56        obligations: Vec::new(),
57        reason: None,
58    }
59}
60
61/// Grant-only policy builder.
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct GrantPolicy<O> {
64    outcome: O,
65    condition: Condition,
66    label: Option<ClauseLabel>,
67    deny_shape: DenyShape,
68    obligations: Vec<ObligationId>,
69    reason: Option<ReasonCode>,
70}
71
72impl<O> GrantPolicy<O> {
73    /// Converts this grant builder into a policy.
74    #[must_use]
75    pub fn into_policy(self) -> Policy<O> {
76        Policy::Grant {
77            outcome: self.outcome,
78            condition: self.condition,
79            label: self.label,
80            deny_shape: self.deny_shape,
81            obligations: self.obligations,
82            reason: self.reason,
83        }
84    }
85
86    /// Adds a label to this grant.
87    #[must_use]
88    pub fn labeled(mut self, label: impl IntoLabel) -> Self {
89        self.label = Some(label.into_label());
90        self
91    }
92
93    /// Tries to add a validated label to this grant.
94    ///
95    /// # Errors
96    ///
97    /// Returns [`crate::GatekeepError::EmptyIdentifier`] when `label` is empty
98    /// or contains only whitespace.
99    pub fn try_labeled(self, label: impl Into<String>) -> GatekeepResult<Self> {
100        Ok(self.labeled(ClauseLabel::new(label)?))
101    }
102
103    /// Marks this grant's denial as hidden.
104    #[must_use]
105    pub const fn hidden(mut self) -> Self {
106        self.deny_shape = DenyShape::Hidden;
107        self
108    }
109
110    /// Adds a reason code to this grant's denial.
111    #[must_use]
112    pub fn reason(mut self, reason: impl IntoReasonCode) -> Self {
113        self.reason = Some(reason.into_reason_code());
114        self
115    }
116
117    /// Tries to add a validated reason code to this grant's denial.
118    ///
119    /// # Errors
120    ///
121    /// Returns [`crate::GatekeepError::EmptyIdentifier`] when `reason` is empty
122    /// or contains only whitespace.
123    pub fn try_reason(self, reason: impl Into<String>) -> GatekeepResult<Self> {
124        Ok(self.reason(ReasonCode::new(reason)?))
125    }
126
127    /// Adds a typed obligation to this grant's permit result.
128    #[must_use]
129    pub fn with_obligation<S: ObligationSpec>(mut self) -> Self {
130        self.obligations
131            .push(ObligationId::from_trusted(S::ID.as_str()));
132        self
133    }
134}
135
136impl<O> From<GrantPolicy<O>> for Policy<O> {
137    fn from(grant: GrantPolicy<O>) -> Self {
138        grant.into_policy()
139    }
140}
141
142/// Builds a meet composition. Empty input fails closed.
143#[must_use]
144pub fn all<O>(policies: impl IntoIterator<Item = Policy<O>>) -> Policy<O> {
145    let policies = policies.into_iter().collect::<Vec<_>>();
146    if policies.is_empty() {
147        Policy::Deny
148    } else {
149        Policy::All(policies)
150    }
151}
152
153/// Builds a join composition. Empty input fails closed.
154#[must_use]
155pub fn any<O>(policies: impl IntoIterator<Item = Policy<O>>) -> Policy<O> {
156    let policies = policies.into_iter().collect::<Vec<_>>();
157    if policies.is_empty() {
158        Policy::Deny
159    } else {
160        Policy::Any(policies)
161    }
162}
163
164/// Builds a fallback policy.
165#[must_use]
166pub fn or_else<O>(primary: Policy<O>, fallback: Policy<O>) -> Policy<O> {
167    Policy::OrElse {
168        primary: Box::new(primary),
169        fallback: Box::new(fallback),
170    }
171}
172
173impl<O> Policy<O> {
174    /// Adds a label to grant policies.
175    #[must_use]
176    pub fn labeled(mut self, label: impl IntoLabel) -> Self {
177        if let Self::Grant {
178            label: grant_label, ..
179        } = &mut self
180        {
181            *grant_label = Some(label.into_label());
182        }
183        self
184    }
185
186    /// Tries to add a validated label to grant policies.
187    ///
188    /// # Errors
189    ///
190    /// Returns [`crate::GatekeepError::EmptyIdentifier`] when `label` is empty
191    /// or contains only whitespace.
192    pub fn try_labeled(self, label: impl Into<String>) -> GatekeepResult<Self> {
193        Ok(self.labeled(ClauseLabel::new(label)?))
194    }
195
196    /// Marks grant denial as hidden.
197    #[must_use]
198    pub const fn hidden(mut self) -> Self {
199        if let Self::Grant { deny_shape, .. } = &mut self {
200            *deny_shape = crate::DenyShape::Hidden;
201        }
202        self
203    }
204
205    /// Adds a reason code to grant denials.
206    #[must_use]
207    pub fn reason(mut self, reason: impl IntoReasonCode) -> Self {
208        if let Self::Grant {
209            reason: grant_reason,
210            ..
211        } = &mut self
212        {
213            *grant_reason = Some(reason.into_reason_code());
214        }
215        self
216    }
217
218    /// Tries to add a validated reason code to grant denials.
219    ///
220    /// # Errors
221    ///
222    /// Returns [`crate::GatekeepError::EmptyIdentifier`] when `reason` is empty
223    /// or contains only whitespace.
224    pub fn try_reason(self, reason: impl Into<String>) -> GatekeepResult<Self> {
225        Ok(self.reason(ReasonCode::new(reason)?))
226    }
227
228    /// Adds a typed obligation to grant permits.
229    #[must_use]
230    pub fn with_obligation<S: ObligationSpec>(mut self) -> Self {
231        if let Self::Grant { obligations, .. } = &mut self {
232            obligations.push(crate::ObligationId::from_trusted(S::ID.as_str()));
233        }
234        self
235    }
236}
237
238/// Conversion into a validated clause label.
239pub trait IntoLabel {
240    /// Converts the value into a label.
241    fn into_label(self) -> ClauseLabel;
242}
243
244impl IntoLabel for ClauseLabel {
245    fn into_label(self) -> ClauseLabel {
246        self
247    }
248}
249
250/// Conversion into a validated reason code.
251pub trait IntoReasonCode {
252    /// Converts the value into a reason code.
253    fn into_reason_code(self) -> ReasonCode;
254}
255
256impl IntoReasonCode for ReasonCode {
257    fn into_reason_code(self) -> ReasonCode {
258        self
259    }
260}