cedar_policy_validator/
validation_result.rs

1/*
2 * Copyright Cedar Contributors
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      https://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17use cedar_policy_core::ast::PolicyID;
18use cedar_policy_core::parser::Loc;
19use miette::Diagnostic;
20use thiserror::Error;
21
22use crate::TypeErrorKind;
23
24/// Contains the result of policy validation. The result includes the list of
25/// issues found by validation and whether validation succeeds or fails.
26/// Validation succeeds if there are no fatal errors. There may still be
27/// non-fatal warnings present when validation passes.
28#[derive(Debug)]
29pub struct ValidationResult<'a> {
30    validation_errors: Vec<ValidationError<'a>>,
31    validation_warnings: Vec<ValidationWarning<'a>>,
32}
33
34impl<'a> ValidationResult<'a> {
35    pub fn new(
36        errors: impl IntoIterator<Item = ValidationError<'a>>,
37        warnings: impl IntoIterator<Item = ValidationWarning<'a>>,
38    ) -> Self {
39        Self {
40            validation_errors: errors.into_iter().collect(),
41            validation_warnings: warnings.into_iter().collect(),
42        }
43    }
44
45    /// True when validation passes. There are no errors, but there may be
46    /// non-fatal warnings.
47    pub fn validation_passed(&self) -> bool {
48        self.validation_errors.is_empty()
49    }
50
51    /// Get an iterator over the errors found by the validator.
52    pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError<'_>> {
53        self.validation_errors.iter()
54    }
55
56    /// Get an iterator over the warnings found by the validator.
57    pub fn validation_warnings(&self) -> impl Iterator<Item = &ValidationWarning<'_>> {
58        self.validation_warnings.iter()
59    }
60
61    /// Get an iterator over the errors and warnings found by the validator.
62    pub fn into_errors_and_warnings(
63        self,
64    ) -> (
65        impl Iterator<Item = ValidationError<'a>>,
66        impl Iterator<Item = ValidationWarning<'a>>,
67    ) {
68        (
69            self.validation_errors.into_iter(),
70            self.validation_warnings.into_iter(),
71        )
72    }
73}
74
75/// An error generated by the validator when it finds a potential problem in a
76/// policy. The error contains a enumeration that specifies the kind of problem,
77/// and provides details specific to that kind of problem. The error also records
78/// where the problem was encountered.
79#[derive(Debug)]
80#[cfg_attr(test, derive(Eq, PartialEq))]
81pub struct ValidationError<'a> {
82    location: SourceLocation<'a>,
83    error_kind: ValidationErrorKind,
84}
85
86impl<'a> ValidationError<'a> {
87    pub(crate) fn with_policy_id(
88        id: &'a PolicyID,
89        source_loc: Option<Loc>,
90        error_kind: ValidationErrorKind,
91    ) -> Self {
92        Self {
93            error_kind,
94            location: SourceLocation::new(id, source_loc),
95        }
96    }
97
98    /// Deconstruct this into its component source location and error kind.
99    pub fn into_location_and_error_kind(self) -> (SourceLocation<'a>, ValidationErrorKind) {
100        (self.location, self.error_kind)
101    }
102
103    /// Extract details about the exact issue detected by the validator.
104    pub fn error_kind(&self) -> &ValidationErrorKind {
105        &self.error_kind
106    }
107
108    /// Extract the location where the validator found the issue.
109    pub fn location(&self) -> &SourceLocation<'_> {
110        &self.location
111    }
112}
113
114/// Represents a location in Cedar policy source.
115#[derive(Debug, Clone, Hash, Eq, PartialEq)]
116pub struct SourceLocation<'a> {
117    policy_id: &'a PolicyID,
118    source_loc: Option<Loc>,
119}
120
121impl<'a> SourceLocation<'a> {
122    pub(crate) fn new(policy_id: &'a PolicyID, source_loc: Option<Loc>) -> Self {
123        Self {
124            policy_id,
125            source_loc,
126        }
127    }
128
129    /// Get the `PolicyId` for the policy at this source location.
130    pub fn policy_id(&self) -> &'a PolicyID {
131        self.policy_id
132    }
133
134    pub fn source_loc(&self) -> Option<&Loc> {
135        self.source_loc.as_ref()
136    }
137}
138
139/// Enumeration of the possible diagnostic error that could be found by the
140/// verification steps.
141#[derive(Debug, Clone, Diagnostic, Error)]
142#[cfg_attr(test, derive(Eq, PartialEq))]
143#[non_exhaustive]
144pub enum ValidationErrorKind {
145    /// A policy contains an entity type that is not declared in the schema.
146    #[error(transparent)]
147    #[diagnostic(transparent)]
148    UnrecognizedEntityType(#[from] UnrecognizedEntityType),
149    /// A policy contains an action that is not declared in the schema.
150    #[error(transparent)]
151    #[diagnostic(transparent)]
152    UnrecognizedActionId(#[from] UnrecognizedActionId),
153    /// There is no action satisfying the action scope constraint that can be
154    /// applied to a principal and resources that both satisfy their respective
155    /// scope conditions.
156    #[error(transparent)]
157    #[diagnostic(transparent)]
158    InvalidActionApplication(#[from] InvalidActionApplication),
159    /// The type checker found an error.
160    #[error(transparent)]
161    #[diagnostic(transparent)]
162    TypeError(#[from] TypeErrorKind),
163    /// An unspecified entity was used in a policy. This should be impossible,
164    /// assuming that the policy was constructed by the parser.
165    #[error(transparent)]
166    #[diagnostic(transparent)]
167    UnspecifiedEntity(#[from] UnspecifiedEntityError),
168}
169
170impl ValidationErrorKind {
171    pub(crate) fn unrecognized_entity_type(
172        actual_entity_type: String,
173        suggested_entity_type: Option<String>,
174    ) -> ValidationErrorKind {
175        UnrecognizedEntityType {
176            actual_entity_type,
177            suggested_entity_type,
178        }
179        .into()
180    }
181
182    pub(crate) fn unrecognized_action_id(
183        actual_action_id: String,
184        suggested_action_id: Option<String>,
185    ) -> ValidationErrorKind {
186        UnrecognizedActionId {
187            actual_action_id,
188            suggested_action_id,
189        }
190        .into()
191    }
192
193    pub(crate) fn invalid_action_application(
194        would_in_fix_principal: bool,
195        would_in_fix_resource: bool,
196    ) -> ValidationErrorKind {
197        InvalidActionApplication {
198            would_in_fix_principal,
199            would_in_fix_resource,
200        }
201        .into()
202    }
203
204    pub(crate) fn type_error(type_error: TypeErrorKind) -> ValidationErrorKind {
205        type_error.into()
206    }
207
208    pub(crate) fn unspecified_entity(entity_id: String) -> ValidationErrorKind {
209        UnspecifiedEntityError { entity_id }.into()
210    }
211}
212
213/// Structure containing details about an unrecognized entity type error.
214#[derive(Debug, Clone, Error)]
215#[cfg_attr(test, derive(Eq, PartialEq))]
216#[error("unrecognized entity type `{actual_entity_type}`")]
217pub struct UnrecognizedEntityType {
218    /// The entity type seen in the policy.
219    pub(crate) actual_entity_type: String,
220    /// An entity type from the schema that the user might reasonably have
221    /// intended to write.
222    pub(crate) suggested_entity_type: Option<String>,
223}
224
225impl Diagnostic for UnrecognizedEntityType {
226    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
227        match &self.suggested_entity_type {
228            Some(s) => Some(Box::new(format!("did you mean `{s}`?"))),
229            None => None,
230        }
231    }
232}
233
234/// Structure containing details about an unrecognized action id error.
235#[derive(Debug, Clone, Error)]
236#[cfg_attr(test, derive(Eq, PartialEq))]
237#[error("unrecognized action `{actual_action_id}`")]
238pub struct UnrecognizedActionId {
239    /// Action Id seen in the policy.
240    pub(crate) actual_action_id: String,
241    /// An action id from the schema that the user might reasonably have
242    /// intended to write.
243    pub(crate) suggested_action_id: Option<String>,
244}
245
246impl Diagnostic for UnrecognizedActionId {
247    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
248        match &self.suggested_action_id {
249            Some(s) => Some(Box::new(format!("did you mean `{s}`?"))),
250            None => None,
251        }
252    }
253}
254
255/// Structure containing details about an invalid action application error.
256#[derive(Debug, Clone, Error)]
257#[cfg_attr(test, derive(Eq, PartialEq))]
258#[error("unable to find an applicable action given the policy scope constraints")]
259pub struct InvalidActionApplication {
260    pub(crate) would_in_fix_principal: bool,
261    pub(crate) would_in_fix_resource: bool,
262}
263
264impl Diagnostic for InvalidActionApplication {
265    fn help<'a>(&'a self) -> Option<Box<dyn std::fmt::Display + 'a>> {
266        match (self.would_in_fix_principal, self.would_in_fix_resource) {
267            (true, false) => Some(Box::new(
268                "try replacing `==` with `in` in the principal clause",
269            )),
270            (false, true) => Some(Box::new(
271                "try replacing `==` with `in` in the resource clause",
272            )),
273            (true, true) => Some(Box::new(
274                "try replacing `==` with `in` in the principal clause and the resource clause",
275            )),
276            (false, false) => None,
277        }
278    }
279}
280
281/// Structure containing details about an unspecified entity error.
282#[derive(Debug, Clone, Diagnostic, Error)]
283#[cfg_attr(test, derive(Eq, PartialEq))]
284#[error("unspecified entity with id `{entity_id}`")]
285#[diagnostic(help("unspecified entities cannot be used in policies"))]
286pub struct UnspecifiedEntityError {
287    /// EID of the unspecified entity.
288    pub(crate) entity_id: String,
289}
290
291/// The structure for validation warnings.
292#[derive(Debug, Clone, Hash, Eq, PartialEq)]
293pub struct ValidationWarning<'a> {
294    pub(crate) location: SourceLocation<'a>,
295    pub(crate) kind: ValidationWarningKind,
296}
297
298impl<'a> ValidationWarning<'a> {
299    pub(crate) fn with_policy_id(
300        id: &'a PolicyID,
301        source_loc: Option<Loc>,
302        warning_kind: ValidationWarningKind,
303    ) -> Self {
304        Self {
305            kind: warning_kind,
306            location: SourceLocation::new(id, source_loc),
307        }
308    }
309
310    pub fn location(&self) -> &SourceLocation<'a> {
311        &self.location
312    }
313
314    pub fn kind(&self) -> &ValidationWarningKind {
315        &self.kind
316    }
317
318    pub fn to_kind_and_location(self) -> (SourceLocation<'a>, ValidationWarningKind) {
319        (self.location, self.kind)
320    }
321}
322
323impl std::fmt::Display for ValidationWarning<'_> {
324    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325        write!(
326            f,
327            "validation warning on policy `{}`: {}",
328            self.location.policy_id(),
329            self.kind
330        )
331    }
332}
333
334/// Represents the different kinds of validation warnings and information
335/// specific to that warning. Marked as `non_exhaustive` to allow adding
336/// additional warnings in the future as a non-breaking change.
337#[derive(Debug, Clone, PartialEq, Diagnostic, Error, Eq, Hash)]
338#[non_exhaustive]
339#[diagnostic(severity(Warning))]
340pub enum ValidationWarningKind {
341    /// A string contains mixed scripts. Different scripts can contain visually similar characters which may be confused for each other.
342    #[error("string `\"{0}\"` contains mixed scripts")]
343    MixedScriptString(String),
344    /// A string contains BIDI control characters. These can be used to create crafted pieces of code that obfuscate true control flow.
345    #[error("string `\"{0}\"` contains BIDI control characters")]
346    BidiCharsInString(String),
347    /// An id contains BIDI control characters. These can be used to create crafted pieces of code that obfuscate true control flow.
348    #[error("identifier `{0}` contains BIDI control characters")]
349    BidiCharsInIdentifier(String),
350    /// An id contains mixed scripts. This can cause characters to be confused for each other.
351    #[error("identifier `{0}` contains mixed scripts")]
352    MixedScriptIdentifier(String),
353    /// An id contains characters that fall outside of the General Security Profile for Identifiers. We recommend adhering to this if possible. See UnicodeĀ® Technical Standard #39 for more info.
354    #[error("identifier `{0}` contains characters that fall outside of the General Security Profile for Identifiers")]
355    ConfusableIdentifier(String),
356    /// The typechecker found that a policy condition will always evaluate to false.
357    #[error(
358        "policy is impossible: the policy expression evaluates to false for all valid requests"
359    )]
360    ImpossiblePolicy,
361}