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    ) -> Self {
351        validation_errors::FunctionArgumentValidation {
352            source_loc,
353            policy_id,
354            msg,
355        }
356        .into()
357    }
358
359    pub(crate) fn empty_set_forbidden(source_loc: Option<Loc>, policy_id: PolicyID) -> Self {
360        validation_errors::EmptySetForbidden {
361            source_loc,
362            policy_id,
363        }
364        .into()
365    }
366
367    pub(crate) fn non_lit_ext_constructor(source_loc: Option<Loc>, policy_id: PolicyID) -> Self {
368        validation_errors::NonLitExtConstructor {
369            source_loc,
370            policy_id,
371        }
372        .into()
373    }
374
375    pub(crate) fn internal_invariant_violation(
376        source_loc: Option<Loc>,
377        policy_id: PolicyID,
378    ) -> Self {
379        validation_errors::InternalInvariantViolation {
380            source_loc,
381            policy_id,
382        }
383        .into()
384    }
385
386    pub(crate) fn invalid_enum_entity(
387        source_loc: Option<Loc>,
388        policy_id: PolicyID,
389        err: InvalidEnumEntityError,
390    ) -> Self {
391        validation_errors::InvalidEnumEntity {
392            source_loc,
393            policy_id,
394            err,
395        }
396        .into()
397    }
398
399    pub(crate) fn maximum_level_exceeded(
400        source_loc: Option<Loc>,
401        policy_id: PolicyID,
402        allowed_level: crate::validator::level_validate::EntityDerefLevel,
403        actual_level: crate::validator::level_validate::EntityDerefLevel,
404    ) -> Self {
405        validation_errors::EntityDerefLevelViolation {
406            source_loc,
407            policy_id,
408            violation_kind: validation_errors::EntityDerefViolationKind::MaximumLevelExceeded {
409                allowed_level,
410                actual_level,
411            },
412        }
413        .into()
414    }
415
416    pub(crate) fn literal_dereference_target(source_loc: Option<Loc>, policy_id: PolicyID) -> Self {
417        validation_errors::EntityDerefLevelViolation {
418            source_loc,
419            policy_id,
420            violation_kind: validation_errors::EntityDerefViolationKind::LiteralDerefTarget,
421        }
422        .into()
423    }
424}
425
426/// Represents the different kinds of validation warnings and information
427/// specific to that warning.
428#[derive(Debug, Clone, PartialEq, Diagnostic, Error, Eq, Hash)]
429pub enum ValidationWarning {
430    /// A string contains mixed scripts. Different scripts can contain visually similar characters which may be confused for each other.
431    #[diagnostic(transparent)]
432    #[error(transparent)]
433    MixedScriptString(#[from] validation_warnings::MixedScriptString),
434    /// A string contains BIDI control characters. These can be used to create crafted pieces of code that obfuscate true control flow.
435    #[diagnostic(transparent)]
436    #[error(transparent)]
437    BidiCharsInString(#[from] validation_warnings::BidiCharsInString),
438    /// An id contains BIDI control characters. These can be used to create crafted pieces of code that obfuscate true control flow.
439    #[diagnostic(transparent)]
440    #[error(transparent)]
441    BidiCharsInIdentifier(#[from] validation_warnings::BidiCharsInIdentifier),
442    /// An id contains mixed scripts. This can cause characters to be confused for each other.
443    #[diagnostic(transparent)]
444    #[error(transparent)]
445    MixedScriptIdentifier(#[from] validation_warnings::MixedScriptIdentifier),
446    /// 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.
447    #[diagnostic(transparent)]
448    #[error(transparent)]
449    ConfusableIdentifier(#[from] validation_warnings::ConfusableIdentifier),
450    /// The typechecker found that a policy condition will always evaluate to false.
451    #[diagnostic(transparent)]
452    #[error(transparent)]
453    ImpossiblePolicy(#[from] validation_warnings::ImpossiblePolicy),
454}
455
456impl ValidationWarning {
457    pub(crate) fn mixed_script_string(
458        source_loc: Option<Loc>,
459        policy_id: PolicyID,
460        string: impl Into<String>,
461    ) -> Self {
462        validation_warnings::MixedScriptString {
463            source_loc,
464            policy_id,
465            string: string.into(),
466        }
467        .into()
468    }
469
470    pub(crate) fn bidi_chars_strings(
471        source_loc: Option<Loc>,
472        policy_id: PolicyID,
473        string: impl Into<String>,
474    ) -> Self {
475        validation_warnings::BidiCharsInString {
476            source_loc,
477            policy_id,
478            string: string.into(),
479        }
480        .into()
481    }
482
483    pub(crate) fn mixed_script_identifier(
484        source_loc: Option<Loc>,
485        policy_id: PolicyID,
486        id: impl Into<String>,
487    ) -> Self {
488        validation_warnings::MixedScriptIdentifier {
489            source_loc,
490            policy_id,
491            id: id.into(),
492        }
493        .into()
494    }
495
496    pub(crate) fn bidi_chars_identifier(
497        source_loc: Option<Loc>,
498        policy_id: PolicyID,
499        id: impl Into<String>,
500    ) -> Self {
501        validation_warnings::BidiCharsInIdentifier {
502            source_loc,
503            policy_id,
504            id: id.into(),
505        }
506        .into()
507    }
508
509    pub(crate) fn confusable_identifier(
510        source_loc: Option<Loc>,
511        policy_id: PolicyID,
512        id: impl Into<String>,
513        confusable_character: char,
514    ) -> Self {
515        validation_warnings::ConfusableIdentifier {
516            source_loc,
517            policy_id,
518            id: id.into(),
519            confusable_character,
520        }
521        .into()
522    }
523
524    pub(crate) fn impossible_policy(source_loc: Option<Loc>, policy_id: PolicyID) -> Self {
525        validation_warnings::ImpossiblePolicy {
526            source_loc,
527            policy_id,
528        }
529        .into()
530    }
531}