1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
use std::fmt;

use indoc::indoc;
use strum_macros::AsRefStr;

use super::sources::Context;
use super::terms::{InstanceLiteral, Pattern, Symbol, Term, Value};

#[derive(Debug)]
pub struct PolarWarning(pub ValidationWarning);

impl PolarWarning {
    pub fn kind(&self) -> String {
        "ValidationWarning::".to_string() + self.0.as_ref()
    }

    pub fn get_context(&self) -> Option<Context> {
        use ValidationWarning::*;

        match &self.0 {
            AmbiguousPrecedence { term } | UnknownSpecializer { term, .. } => {
                term.parsed_context().cloned()
            }
            MissingAllowRule | MissingHasPermissionRule => None,
        }
    }
}

impl fmt::Display for PolarWarning {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0)?;
        if let Some(context) = self.get_context() {
            write!(f, "{}", context)?;
        }
        Ok(())
    }
}

#[derive(AsRefStr, Debug)]
pub enum ValidationWarning {
    // Category: general
    AmbiguousPrecedence { term: Term },
    // Category: enforcement
    MissingAllowRule,
    // Category: resource blocks
    MissingHasPermissionRule,
    // Category: general
    // TODO(gj): won't need `sym` once we have an easier, infallible way of going from `Term` ->
    // `Pattern` -> `InstanceLiteral` -> `tag` (`Symbol`).
    UnknownSpecializer { term: Term, sym: Symbol },
}

impl From<ValidationWarning> for PolarWarning {
    fn from(other: ValidationWarning) -> Self {
        Self(other)
    }
}

const AMBIGUOUS_PRECEDENCE_MSG: &str = indoc! {"
    Expression without parentheses could be ambiguous.
    Prior to 0.20, `x and y or z` would parse as `x and (y or z)`.
    As of 0.20, it parses as `(x and y) or z`, matching other languages.
"};

const MISSING_ALLOW_RULE_MSG: &str = indoc! {"
    Your policy does not contain an allow rule, which usually means
    that no actions are allowed. Did you mean to add an allow rule to
    the top of your policy?

      allow(actor, action, resource) if ...

    You can also suppress this warning by adding an allow_field or allow_request
    rule. For more information about allow rules, see:

      https://docs.osohq.com/reference/polar/builtin_rule_types.html#allow
"};

const MISSING_HAS_PERMISSION_RULE_MSG: &str = indoc! {"
    Warning: your policy uses resource blocks but does not call the
    has_permission rule. This means that permissions you define in a
    resource block will not have any effect. Did you mean to include a
    call to has_permission in a top-level allow rule?

      allow(actor, action, resource) if
          has_permission(actor, action, resource);

    For more information about resource blocks, see https://docs.osohq.com/any/reference/polar/polar-syntax.html#actor-and-resource-blocks
"};

fn common_specializer_misspellings(term: &Term) -> Option<&str> {
    if let Value::Pattern(Pattern::Instance(InstanceLiteral { tag, .. })) = term.value() {
        let misspelled_type = match tag.0.as_ref() {
            "integer" => "Integer",
            "int" => "Integer",
            "i32" => "Integer",
            "i64" => "Integer",
            "u32" => "Integer",
            "u64" => "Integer",
            "usize" => "Integer",
            "size_t" => "Integer",
            "float" => "Float",
            "f32" => "Float",
            "f64" => "Float",
            "double" => "Float",
            "char" => "String",
            "str" => "String",
            "string" => "String",
            "list" => "List",
            "array" => "List",
            "Array" => "List",
            "dict" => "Dictionary",
            "Dict" => "Dictionary",
            "dictionary" => "Dictionary",
            "hash" => "Dictionary",
            "Hash" => "Dictionary",
            "map" => "Dictionary",
            "Map" => "Dictionary",
            "HashMap" => "Dictionary",
            "hashmap" => "Dictionary",
            "hash_map" => "Dictionary",
            _ => return None,
        };
        return Some(misspelled_type);
    }
    None
}

impl fmt::Display for ValidationWarning {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        use ValidationWarning::*;

        match self {
            AmbiguousPrecedence { .. } => write!(f, "{}", AMBIGUOUS_PRECEDENCE_MSG)?,
            MissingAllowRule => write!(f, "{}", MISSING_ALLOW_RULE_MSG)?,
            MissingHasPermissionRule => write!(f, "{}", MISSING_HAS_PERMISSION_RULE_MSG)?,
            UnknownSpecializer { term, sym } => {
                write!(f, "Unknown specializer {}", sym)?;
                if let Some(suggestion) = common_specializer_misspellings(term) {
                    write!(f, ", did you mean {}?", suggestion)?;
                }
            }
        }

        Ok(())
    }
}