cedar_policy_validator/
diagnostics.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
17//! This module contains the diagnostics (i.e., errors and warnings) that are
18//! returned by the validator.
19
20use miette::Diagnostic;
21use thiserror::Error;
22
23use std::collections::BTreeSet;
24
25use cedar_policy_core::ast::{EntityType, PolicyID};
26use cedar_policy_core::parser::Loc;
27
28use crate::types::Type;
29
30pub mod validation_errors;
31pub mod validation_warnings;
32
33/// Contains the result of policy validation. The result includes the list of
34/// issues found by validation and whether validation succeeds or fails.
35/// Validation succeeds if there are no fatal errors. There may still be
36/// non-fatal warnings present when validation passes.
37#[derive(Debug)]
38pub struct ValidationResult {
39    validation_errors: Vec<ValidationError>,
40    validation_warnings: Vec<ValidationWarning>,
41}
42
43impl ValidationResult {
44    /// Create a new `ValidationResult` with these errors and warnings.
45    /// Empty iterators are allowed for either or both arguments.
46    pub fn new(
47        errors: impl IntoIterator<Item = ValidationError>,
48        warnings: impl IntoIterator<Item = ValidationWarning>,
49    ) -> Self {
50        Self {
51            validation_errors: errors.into_iter().collect(),
52            validation_warnings: warnings.into_iter().collect(),
53        }
54    }
55
56    /// True when validation passes. There are no errors, but there may be
57    /// non-fatal warnings.
58    pub fn validation_passed(&self) -> bool {
59        self.validation_errors.is_empty()
60    }
61
62    /// Get an iterator over the errors found by the validator.
63    pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError> {
64        self.validation_errors.iter()
65    }
66
67    /// Get an iterator over the warnings found by the validator.
68    pub fn validation_warnings(&self) -> impl Iterator<Item = &ValidationWarning> {
69        self.validation_warnings.iter()
70    }
71
72    /// Get an iterator over the errors and warnings found by the validator.
73    pub fn into_errors_and_warnings(
74        self,
75    ) -> (
76        impl Iterator<Item = ValidationError>,
77        impl Iterator<Item = ValidationWarning>,
78    ) {
79        (
80            self.validation_errors.into_iter(),
81            self.validation_warnings.into_iter(),
82        )
83    }
84}
85
86/// An error generated by the validator when it finds a potential problem in a
87/// policy. The error contains a enumeration that specifies the kind of problem,
88/// and provides details specific to that kind of problem. The error also records
89/// where the problem was encountered.
90//
91// This is NOT a publicly exported error type.
92#[derive(Clone, Debug, Diagnostic, Error, Hash, Eq, PartialEq)]
93pub enum ValidationError {
94    /// A policy contains an entity type that is not declared in the schema.
95    #[error(transparent)]
96    #[diagnostic(transparent)]
97    UnrecognizedEntityType(#[from] validation_errors::UnrecognizedEntityType),
98    /// A policy contains an action that is not declared in the schema.
99    #[error(transparent)]
100    #[diagnostic(transparent)]
101    UnrecognizedActionId(#[from] validation_errors::UnrecognizedActionId),
102    /// There is no action satisfying the action scope constraint that can be
103    /// applied to a principal and resources that both satisfy their respective
104    /// scope conditions.
105    #[error(transparent)]
106    #[diagnostic(transparent)]
107    InvalidActionApplication(#[from] validation_errors::InvalidActionApplication),
108    /// The typechecker expected to see a subtype of one of the types in
109    /// `expected`, but saw `actual`.
110    #[error(transparent)]
111    #[diagnostic(transparent)]
112    UnexpectedType(#[from] validation_errors::UnexpectedType),
113    /// The typechecker could not compute a least upper bound for `types`.
114    #[error(transparent)]
115    #[diagnostic(transparent)]
116    IncompatibleTypes(#[from] validation_errors::IncompatibleTypes),
117    /// The typechecker detected an access to a record or entity attribute
118    /// that it could not statically guarantee would be present.
119    #[error(transparent)]
120    #[diagnostic(transparent)]
121    UnsafeAttributeAccess(#[from] validation_errors::UnsafeAttributeAccess),
122    /// The typechecker could not conclude that an access to an optional
123    /// attribute was safe.
124    #[error(transparent)]
125    #[diagnostic(transparent)]
126    UnsafeOptionalAttributeAccess(#[from] validation_errors::UnsafeOptionalAttributeAccess),
127    /// Undefined extension function.
128    #[error(transparent)]
129    #[diagnostic(transparent)]
130    UndefinedFunction(#[from] validation_errors::UndefinedFunction),
131    /// Incorrect number of arguments in an extension function application.
132    #[error(transparent)]
133    #[diagnostic(transparent)]
134    WrongNumberArguments(#[from] validation_errors::WrongNumberArguments),
135    /// Incorrect call style in an extension function application.
136    /// Error returned by custom extension function argument validation
137    #[diagnostic(transparent)]
138    #[error(transparent)]
139    FunctionArgumentValidation(#[from] validation_errors::FunctionArgumentValidation),
140    /// The policy uses an empty set literal in a way that is forbidden
141    #[diagnostic(transparent)]
142    #[error(transparent)]
143    EmptySetForbidden(#[from] validation_errors::EmptySetForbidden),
144    /// The policy passes a non-literal to an extension constructor, which is
145    /// forbidden in strict validation
146    #[diagnostic(transparent)]
147    #[error(transparent)]
148    NonLitExtConstructor(#[from] validation_errors::NonLitExtConstructor),
149    /// To pass strict validation a policy cannot contain an `in` expression
150    /// where the entity type on the left might not be able to be a member of
151    /// the entity type on the right.
152    #[error(transparent)]
153    #[diagnostic(transparent)]
154    HierarchyNotRespected(#[from] validation_errors::HierarchyNotRespected),
155}
156
157impl ValidationError {
158    pub(crate) fn unrecognized_entity_type(
159        source_loc: Option<Loc>,
160        policy_id: PolicyID,
161        actual_entity_type: String,
162        suggested_entity_type: Option<String>,
163    ) -> Self {
164        validation_errors::UnrecognizedEntityType {
165            source_loc,
166            policy_id,
167            actual_entity_type,
168            suggested_entity_type,
169        }
170        .into()
171    }
172
173    pub(crate) fn unrecognized_action_id(
174        source_loc: Option<Loc>,
175
176        policy_id: PolicyID,
177        actual_action_id: String,
178        suggested_action_id: Option<String>,
179    ) -> Self {
180        validation_errors::UnrecognizedActionId {
181            source_loc,
182            policy_id,
183            actual_action_id,
184            suggested_action_id,
185        }
186        .into()
187    }
188
189    pub(crate) fn invalid_action_application(
190        source_loc: Option<Loc>,
191        policy_id: PolicyID,
192        would_in_fix_principal: bool,
193        would_in_fix_resource: bool,
194    ) -> Self {
195        validation_errors::InvalidActionApplication {
196            source_loc,
197            policy_id,
198            would_in_fix_principal,
199            would_in_fix_resource,
200        }
201        .into()
202    }
203
204    /// Construct a type error for when an unexpected type occurs in an expression.
205    pub(crate) fn expected_one_of_types(
206        source_loc: Option<Loc>,
207        policy_id: PolicyID,
208        expected: impl IntoIterator<Item = Type>,
209        actual: Type,
210        help: Option<validation_errors::UnexpectedTypeHelp>,
211    ) -> Self {
212        validation_errors::UnexpectedType {
213            source_loc,
214            policy_id,
215            expected: expected.into_iter().collect::<BTreeSet<_>>(),
216            actual,
217            help,
218        }
219        .into()
220    }
221
222    /// Construct a type error for when a least upper bound cannot be found for
223    /// a collection of types.
224    pub(crate) fn incompatible_types(
225        source_loc: Option<Loc>,
226        policy_id: PolicyID,
227        types: impl IntoIterator<Item = Type>,
228        hint: validation_errors::LubHelp,
229        context: validation_errors::LubContext,
230    ) -> Self {
231        validation_errors::IncompatibleTypes {
232            source_loc,
233            policy_id,
234            types: types.into_iter().collect::<BTreeSet<_>>(),
235            hint,
236            context,
237        }
238        .into()
239    }
240
241    pub(crate) fn unsafe_attribute_access(
242        source_loc: Option<Loc>,
243        policy_id: PolicyID,
244        attribute_access: validation_errors::AttributeAccess,
245        suggestion: Option<String>,
246        may_exist: bool,
247    ) -> Self {
248        validation_errors::UnsafeAttributeAccess {
249            source_loc,
250            policy_id,
251            attribute_access,
252            suggestion,
253            may_exist,
254        }
255        .into()
256    }
257
258    pub(crate) fn unsafe_optional_attribute_access(
259        source_loc: Option<Loc>,
260        policy_id: PolicyID,
261        attribute_access: validation_errors::AttributeAccess,
262    ) -> Self {
263        validation_errors::UnsafeOptionalAttributeAccess {
264            source_loc,
265            policy_id,
266            attribute_access,
267        }
268        .into()
269    }
270
271    pub(crate) fn undefined_extension(
272        source_loc: Option<Loc>,
273        policy_id: PolicyID,
274        name: String,
275    ) -> Self {
276        validation_errors::UndefinedFunction {
277            source_loc,
278            policy_id,
279            name,
280        }
281        .into()
282    }
283
284    pub(crate) fn wrong_number_args(
285        source_loc: Option<Loc>,
286
287        policy_id: PolicyID,
288        expected: usize,
289        actual: usize,
290    ) -> Self {
291        validation_errors::WrongNumberArguments {
292            source_loc,
293            policy_id,
294            expected,
295            actual,
296        }
297        .into()
298    }
299
300    pub(crate) fn function_argument_validation(
301        source_loc: Option<Loc>,
302        policy_id: PolicyID,
303        msg: String,
304    ) -> Self {
305        validation_errors::FunctionArgumentValidation {
306            source_loc,
307            policy_id,
308            msg,
309        }
310        .into()
311    }
312
313    pub(crate) fn empty_set_forbidden(source_loc: Option<Loc>, policy_id: PolicyID) -> Self {
314        validation_errors::EmptySetForbidden {
315            source_loc,
316            policy_id,
317        }
318        .into()
319    }
320
321    pub(crate) fn non_lit_ext_constructor(source_loc: Option<Loc>, policy_id: PolicyID) -> Self {
322        validation_errors::NonLitExtConstructor {
323            source_loc,
324            policy_id,
325        }
326        .into()
327    }
328
329    pub(crate) fn hierarchy_not_respected(
330        source_loc: Option<Loc>,
331
332        policy_id: PolicyID,
333        in_lhs: Option<EntityType>,
334        in_rhs: Option<EntityType>,
335    ) -> Self {
336        validation_errors::HierarchyNotRespected {
337            source_loc,
338            policy_id,
339            in_lhs,
340            in_rhs,
341        }
342        .into()
343    }
344}
345
346/// Represents the different kinds of validation warnings and information
347/// specific to that warning.
348#[derive(Debug, Clone, PartialEq, Diagnostic, Error, Eq, Hash)]
349pub enum ValidationWarning {
350    /// A string contains mixed scripts. Different scripts can contain visually similar characters which may be confused for each other.
351    #[diagnostic(transparent)]
352    #[error(transparent)]
353    MixedScriptString(#[from] validation_warnings::MixedScriptString),
354    /// A string contains BIDI control characters. These can be used to create crafted pieces of code that obfuscate true control flow.
355    #[diagnostic(transparent)]
356    #[error(transparent)]
357    BidiCharsInString(#[from] validation_warnings::BidiCharsInString),
358    /// An id contains BIDI control characters. These can be used to create crafted pieces of code that obfuscate true control flow.
359    #[diagnostic(transparent)]
360    #[error(transparent)]
361    BidiCharsInIdentifier(#[from] validation_warnings::BidiCharsInIdentifier),
362    /// An id contains mixed scripts. This can cause characters to be confused for each other.
363    #[diagnostic(transparent)]
364    #[error(transparent)]
365    MixedScriptIdentifier(#[from] validation_warnings::MixedScriptIdentifier),
366    /// 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.
367    #[diagnostic(transparent)]
368    #[error(transparent)]
369    ConfusableIdentifier(#[from] validation_warnings::ConfusableIdentifier),
370    /// The typechecker found that a policy condition will always evaluate to false.
371    #[diagnostic(transparent)]
372    #[error(transparent)]
373    ImpossiblePolicy(#[from] validation_warnings::ImpossiblePolicy),
374}
375
376impl ValidationWarning {
377    pub(crate) fn mixed_script_string(
378        source_loc: Option<Loc>,
379        policy_id: PolicyID,
380        string: impl Into<String>,
381    ) -> Self {
382        validation_warnings::MixedScriptString {
383            source_loc,
384            policy_id,
385            string: string.into(),
386        }
387        .into()
388    }
389
390    pub(crate) fn bidi_chars_strings(
391        source_loc: Option<Loc>,
392        policy_id: PolicyID,
393        string: impl Into<String>,
394    ) -> Self {
395        validation_warnings::BidiCharsInString {
396            source_loc,
397            policy_id,
398            string: string.into(),
399        }
400        .into()
401    }
402
403    pub(crate) fn mixed_script_identifier(
404        source_loc: Option<Loc>,
405        policy_id: PolicyID,
406        id: impl Into<String>,
407    ) -> Self {
408        validation_warnings::MixedScriptIdentifier {
409            source_loc,
410            policy_id,
411            id: id.into(),
412        }
413        .into()
414    }
415
416    pub(crate) fn bidi_chars_identifier(
417        source_loc: Option<Loc>,
418        policy_id: PolicyID,
419        id: impl Into<String>,
420    ) -> Self {
421        validation_warnings::BidiCharsInIdentifier {
422            source_loc,
423            policy_id,
424            id: id.into(),
425        }
426        .into()
427    }
428
429    pub(crate) fn confusable_identifier(
430        source_loc: Option<Loc>,
431        policy_id: PolicyID,
432        id: impl Into<String>,
433    ) -> Self {
434        validation_warnings::ConfusableIdentifier {
435            source_loc,
436            policy_id,
437            id: id.into(),
438        }
439        .into()
440    }
441
442    pub(crate) fn impossible_policy(source_loc: Option<Loc>, policy_id: PolicyID) -> Self {
443        validation_warnings::ImpossiblePolicy {
444            source_loc,
445            policy_id,
446        }
447        .into()
448    }
449}