Skip to main content

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