Skip to content

Commit 9874bc1

Browse files
Report source location for unknown entity type and action errors (#802)
Signed-off-by: John Kastner <[email protected]>
1 parent 036efc2 commit 9874bc1

File tree

12 files changed

+208
-40
lines changed

12 files changed

+208
-40
lines changed

cedar-policy-core/src/ast/entity.rs

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use crate::ast::*;
1818
use crate::evaluator::{EvaluationError, RestrictedEvaluator};
1919
use crate::extensions::Extensions;
2020
use crate::parser::err::ParseErrors;
21+
use crate::parser::Loc;
2122
use crate::transitive_closure::TCNode;
2223
use crate::FromNormalizedStr;
2324
use itertools::Itertools;
@@ -62,13 +63,43 @@ impl std::fmt::Display for EntityType {
6263
}
6364

6465
/// Unique ID for an entity. These represent entities in the AST.
65-
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Hash, PartialOrd, Ord)]
66-
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
66+
#[derive(Serialize, Deserialize, Debug, Clone)]
6767
pub struct EntityUID {
6868
/// Typename of the entity
6969
ty: EntityType,
7070
/// EID of the entity
7171
eid: Eid,
72+
/// Location of the entity in policy source
73+
#[serde(skip)]
74+
loc: Option<Loc>,
75+
}
76+
77+
/// `PartialEq` implementation ignores the `loc`.
78+
impl PartialEq for EntityUID {
79+
fn eq(&self, other: &Self) -> bool {
80+
self.ty == other.ty && self.eid == other.eid
81+
}
82+
}
83+
impl Eq for EntityUID {}
84+
85+
impl std::hash::Hash for EntityUID {
86+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
87+
// hash the ty and eid, in line with the `PartialEq` impl which compares
88+
// the ty and eid.
89+
self.ty.hash(state);
90+
self.eid.hash(state);
91+
}
92+
}
93+
94+
impl PartialOrd for EntityUID {
95+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
96+
Some(self.cmp(other))
97+
}
98+
}
99+
impl Ord for EntityUID {
100+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
101+
self.ty.cmp(&other.ty).then(self.eid.cmp(&other.eid))
102+
}
72103
}
73104

74105
impl StaticallyTyped for EntityUID {
@@ -87,6 +118,7 @@ impl EntityUID {
87118
Self {
88119
ty: Self::test_entity_type(),
89120
eid: Eid(eid.into()),
121+
loc: None,
90122
}
91123
}
92124
// by default, Coverlay does not track coverage for lines after a line
@@ -113,6 +145,7 @@ impl EntityUID {
113145
Ok(Self {
114146
ty: EntityType::Specified(Name::parse_unqualified_name(typename)?),
115147
eid: Eid(eid.into()),
148+
loc: None,
116149
})
117150
}
118151

@@ -122,11 +155,17 @@ impl EntityUID {
122155
(self.ty, self.eid)
123156
}
124157

158+
/// Get the source location for this `EntityUID`.
159+
pub fn loc(&self) -> Option<&Loc> {
160+
self.loc.as_ref()
161+
}
162+
125163
/// Create a nominally-typed `EntityUID` with the given typename and EID
126-
pub fn from_components(name: Name, eid: Eid) -> Self {
164+
pub fn from_components(name: Name, eid: Eid, loc: Option<Loc>) -> Self {
127165
Self {
128166
ty: EntityType::Specified(name),
129167
eid,
168+
loc,
130169
}
131170
}
132171

@@ -135,6 +174,7 @@ impl EntityUID {
135174
Self {
136175
ty: EntityType::Unspecified,
137176
eid,
177+
loc: None,
138178
}
139179
}
140180

@@ -175,6 +215,17 @@ impl FromNormalizedStr for EntityUID {
175215
}
176216
}
177217

218+
#[cfg(feature = "arbitrary")]
219+
impl<'a> arbitrary::Arbitrary<'a> for EntityUID {
220+
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
221+
Ok(Self {
222+
ty: u.arbitrary()?,
223+
eid: u.arbitrary()?,
224+
loc: None,
225+
})
226+
}
227+
}
228+
178229
/// EID type is just a SmolStr for now
179230
#[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Hash, PartialOrd, Ord)]
180231
pub struct Eid(SmolStr);
@@ -497,12 +548,14 @@ mod test {
497548
let e2 = EntityUID::from_components(
498549
Name::parse_unqualified_name("test_entity_type").expect("should be a valid identifier"),
499550
Eid("foo".into()),
551+
None,
500552
);
501553
let e3 = EntityUID::unspecified_from_eid(Eid("foo".into()));
502554
let e4 = EntityUID::unspecified_from_eid(Eid("bar".into()));
503555
let e5 = EntityUID::from_components(
504556
Name::parse_unqualified_name("Unspecified").expect("should be a valid identifier"),
505557
Eid("foo".into()),
558+
None,
506559
);
507560

508561
// an EUID is equal to itself

cedar-policy-core/src/ast/name.rs

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,42 @@ use super::PrincipalOrResource;
2929
/// This is the `Name` type used to name types, functions, etc.
3030
/// The name can include namespaces.
3131
/// Clone is O(1).
32-
#[derive(Debug, PartialEq, Eq, Clone, Hash, PartialOrd, Ord)]
33-
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
32+
#[derive(Debug, Clone)]
3433
pub struct Name {
3534
/// Basename
3635
pub(crate) id: Id,
3736
/// Namespaces
3837
pub(crate) path: Arc<Vec<Id>>,
38+
/// Location of the name in source
39+
pub(crate) loc: Option<Loc>,
40+
}
41+
42+
/// `PartialEq` implementation ignores the `loc`.
43+
impl PartialEq for Name {
44+
fn eq(&self, other: &Self) -> bool {
45+
self.id == other.id && self.path == other.path
46+
}
47+
}
48+
impl Eq for Name {}
49+
50+
impl std::hash::Hash for Name {
51+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
52+
// hash the ty and eid, in line with the `PartialEq` impl which compares
53+
// the ty and eid.
54+
self.id.hash(state);
55+
self.path.hash(state);
56+
}
57+
}
58+
59+
impl PartialOrd for Name {
60+
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
61+
Some(self.cmp(other))
62+
}
63+
}
64+
impl Ord for Name {
65+
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
66+
self.id.cmp(&other.id).then(self.path.cmp(&other.path))
67+
}
3968
}
4069

4170
/// A shortcut for `Name::unqualified_name`
@@ -61,10 +90,11 @@ impl TryFrom<Name> for Id {
6190

6291
impl Name {
6392
/// A full constructor for `Name`
64-
pub fn new(basename: Id, path: impl IntoIterator<Item = Id>) -> Self {
93+
pub fn new(basename: Id, path: impl IntoIterator<Item = Id>, loc: Option<Loc>) -> Self {
6594
Self {
6695
id: basename,
6796
path: Arc::new(path.into_iter().collect()),
97+
loc,
6898
}
6999
}
70100

@@ -73,6 +103,7 @@ impl Name {
73103
Self {
74104
id,
75105
path: Arc::new(vec![]),
106+
loc: None,
76107
}
77108
}
78109

@@ -82,15 +113,21 @@ impl Name {
82113
Ok(Self {
83114
id: s.parse()?,
84115
path: Arc::new(vec![]),
116+
loc: None,
85117
})
86118
}
87119

88120
/// Given a type basename and a namespace (as a `Name` itself),
89121
/// return a `Name` representing the type's fully qualified name
90-
pub fn type_in_namespace(basename: Id, namespace: Name) -> Name {
122+
pub fn type_in_namespace(basename: Id, namespace: Name, loc: Option<Loc>) -> Name {
91123
let mut path = Arc::unwrap_or_clone(namespace.path);
92124
path.push(namespace.id);
93-
Name::new(basename, path)
125+
Name::new(basename, path, loc)
126+
}
127+
128+
/// Get the source location
129+
pub fn loc(&self) -> Option<&Loc> {
130+
self.loc.as_ref()
94131
}
95132

96133
/// Get the basename of the `Name` (ie, with namespaces stripped).
@@ -129,6 +166,7 @@ impl Name {
129166
.namespace_components()
130167
.chain(std::iter::once(namespace.basename()))
131168
.cloned(),
169+
self.loc().cloned(),
132170
),
133171
None => self.clone(),
134172
}
@@ -208,6 +246,17 @@ impl<'de> Deserialize<'de> for Name {
208246
}
209247
}
210248

249+
#[cfg(feature = "arbitrary")]
250+
impl<'a> arbitrary::Arbitrary<'a> for Name {
251+
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
252+
Ok(Self {
253+
id: u.arbitrary()?,
254+
path: u.arbitrary()?,
255+
loc: None,
256+
})
257+
}
258+
}
259+
211260
/// Identifier for a slot
212261
/// Clone is O(1).
213262
// This simply wraps a separate enum -- currently `ValidSlotId` -- in case we

cedar-policy-core/src/ast/policy.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2137,6 +2137,7 @@ mod test {
21372137
let id = EntityUID::from_components(
21382138
name::Name::unqualified_name(id::Id::new_unchecked("s")),
21392139
entity::Eid::new("eid"),
2140+
None,
21402141
);
21412142
let mut i = EntityIterator::One(&id);
21422143
assert_eq!(i.next(), Some(&id));
@@ -2148,10 +2149,12 @@ mod test {
21482149
let id1 = EntityUID::from_components(
21492150
name::Name::unqualified_name(id::Id::new_unchecked("s")),
21502151
entity::Eid::new("eid1"),
2152+
None,
21512153
);
21522154
let id2 = EntityUID::from_components(
21532155
name::Name::unqualified_name(id::Id::new_unchecked("s")),
21542156
entity::Eid::new("eid2"),
2157+
None,
21552158
);
21562159
let v = vec![&id1, &id2];
21572160
let mut i = EntityIterator::Bunch(v);

cedar-policy-core/src/entities/json/value.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ impl TryFrom<TypeAndId> for EntityUID {
185185
Ok(EntityUID::from_components(
186186
Name::from_normalized_str(&e.entity_type)?,
187187
Eid::new(e.id),
188+
None,
188189
))
189190
}
190191
}

cedar-policy-core/src/est/expr.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,6 +1038,7 @@ fn interpret_primary(
10381038
__entity: TypeAndId::from(ast::EntityUID::from_components(
10391039
name,
10401040
ast::Eid::new(eid.clone()),
1041+
None,
10411042
)),
10421043
})))
10431044
}
@@ -1065,6 +1066,7 @@ fn interpret_primary(
10651066
.and_then(|id| id.to_string().parse().map_err(Into::into))
10661067
})
10671068
.collect::<Result<Vec<ast::Id>, ParseErrors>>()?,
1069+
Some(node.loc.clone()),
10681070
))),
10691071
(path, id) => {
10701072
let (l, r, src) = match (path.first(), path.last()) {

cedar-policy-core/src/parser/cst_to_ast.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2118,7 +2118,9 @@ impl Node<Option<cst::Name>> {
21182118

21192119
// computation and error generation is complete, so fail or construct
21202120
match (maybe_name, path.len()) {
2121-
(Some(r), len) if len == name.path.len() => Some(construct_name(path, r)),
2121+
(Some(r), len) if len == name.path.len() => {
2122+
Some(construct_name(path, r, self.loc.clone()))
2123+
}
21222124
_ => None,
21232125
}
21242126
}
@@ -2227,7 +2229,7 @@ impl Node<Option<cst::Ref>> {
22272229
};
22282230

22292231
match (maybe_path, maybe_eid) {
2230-
(Some(p), Some(e)) => Some(construct_refr(p, e)),
2232+
(Some(p), Some(e)) => Some(construct_refr(p, e, self.loc.clone())),
22312233
_ => None,
22322234
}
22332235
}
@@ -2343,15 +2345,16 @@ fn construct_string_from_var(v: ast::Var) -> SmolStr {
23432345
ast::Var::Context => "context".into(),
23442346
}
23452347
}
2346-
fn construct_name(path: Vec<ast::Id>, id: ast::Id) -> ast::Name {
2348+
fn construct_name(path: Vec<ast::Id>, id: ast::Id, loc: Loc) -> ast::Name {
23472349
ast::Name {
23482350
id,
23492351
path: Arc::new(path),
2352+
loc: Some(loc),
23502353
}
23512354
}
2352-
fn construct_refr(p: ast::Name, n: SmolStr) -> ast::EntityUID {
2355+
fn construct_refr(p: ast::Name, n: SmolStr, loc: Loc) -> ast::EntityUID {
23532356
let eid = ast::Eid::new(n);
2354-
ast::EntityUID::from_components(p, eid)
2357+
ast::EntityUID::from_components(p, eid, Some(loc))
23552358
}
23562359
fn construct_expr_ref(r: ast::EntityUID, loc: Loc) -> ast::Expr {
23572360
ast::ExprBuilder::new().with_source_loc(loc).val(r)

cedar-policy-validator/src/human_schema/ast.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ struct PathInternal {
125125

126126
impl From<PathInternal> for Name {
127127
fn from(value: PathInternal) -> Self {
128-
Self::new(value.basename, value.namespace)
128+
Self::new(value.basename, value.namespace, None)
129129
}
130130
}
131131

cedar-policy-validator/src/human_schema/to_json_schema.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,7 @@ impl<'a> ConversionContext<'a> {
434434
&Some(Name::new(
435435
prefix_base.clone(),
436436
prefix_prefix.into_iter().cloned(),
437+
None,
437438
)),
438439
),
439440
None =>

cedar-policy-validator/src/lib.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,6 @@ impl Validator {
135135
Some(
136136
self.validate_entity_types(p)
137137
.chain(self.validate_action_ids(p))
138-
.map(move |note| ValidationError::with_policy_id(p.id(), None, note))
139138
// We could usefully update this pass to apply to partial
140139
// schema if it only failed when there is a known action
141140
// applied to known principal/resource entity types that are
@@ -295,6 +294,8 @@ mod test {
295294
Some("Action::\"action\"".to_string()),
296295
),
297296
);
297+
298+
assert!(!result.validation_passed());
298299
assert!(result
299300
.validation_errors()
300301
.any(|x| x.error_kind() == principal_err.error_kind()));
@@ -379,6 +380,7 @@ mod test {
379380
ast::EntityUID::from_components(
380381
"some_namespace::Photo".parse().unwrap(),
381382
ast::Eid::new("foo"),
383+
None,
382384
),
383385
);
384386
set.link(
@@ -397,6 +399,7 @@ mod test {
397399
ast::EntityUID::from_components(
398400
"some_namespace::Undefined".parse().unwrap(),
399401
ast::Eid::new("foo"),
402+
None,
400403
),
401404
);
402405
set.link(
@@ -432,6 +435,7 @@ mod test {
432435
ast::EntityUID::from_components(
433436
"some_namespace::User".parse().unwrap(),
434437
ast::Eid::new("foo"),
438+
None,
435439
),
436440
);
437441
set.link(
@@ -450,7 +454,9 @@ mod test {
450454
loc.clone(),
451455
ValidationErrorKind::invalid_action_application(false, false),
452456
);
453-
assert!(result.validation_errors().any(|x| x == &invalid_action_err));
457+
assert!(result
458+
.validation_errors()
459+
.any(|x| x.error_kind() == invalid_action_err.error_kind()));
454460

455461
Ok(())
456462
}

0 commit comments

Comments
 (0)