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