cedar_policy_core/entities/json/
entities.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
17use super::{
18    err::{JsonDeserializationError, JsonDeserializationErrorContext, JsonSerializationError},
19    CedarValueJson, EntityTypeDescription, EntityUidJson, NoEntitiesSchema, Schema, TypeAndId,
20    ValueParser,
21};
22use crate::ast::{BorrowedRestrictedExpr, Entity, EntityUID, PartialValue, RestrictedExpr};
23use crate::entities::conformance::EntitySchemaConformanceChecker;
24use crate::entities::{
25    conformance::err::{EntitySchemaConformanceError, UnexpectedEntityTypeError},
26    Entities, EntitiesError, TCComputation,
27};
28use crate::extensions::Extensions;
29use crate::jsonvalue::JsonValueWithNoDuplicateKeys;
30use serde::{Deserialize, Serialize};
31use serde_with::serde_as;
32use smol_str::SmolStr;
33use std::sync::Arc;
34use std::{
35    collections::{HashMap, HashSet},
36    io::Read,
37};
38
39#[cfg(feature = "wasm")]
40extern crate tsify;
41
42/// Serde JSON format for a single entity
43#[serde_as]
44#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
45#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
46#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
47pub struct EntityJson {
48    /// UID of the entity, specified in any form accepted by `EntityUidJson`
49    uid: EntityUidJson,
50    /// attributes, whose values can be any JSON value.
51    /// (Probably a `CedarValueJson`, but for schema-based parsing, it could for
52    /// instance be an `EntityUidJson` if we're expecting an entity reference,
53    /// so for now we leave it in its raw json-value form, albeit not allowing
54    /// any duplicate keys in any records that may occur in an attribute value
55    /// (even nested).)
56    #[serde_as(as = "serde_with::MapPreventDuplicates<_,_>")]
57    #[cfg_attr(feature = "wasm", tsify(type = "Record<string, CedarValueJson>"))]
58    // the annotation covers duplicates in this `HashMap` itself, while the `JsonValueWithNoDuplicateKeys` covers duplicates in any records contained in attribute values (including recursively)
59    attrs: HashMap<SmolStr, JsonValueWithNoDuplicateKeys>,
60    /// Parents of the entity, specified in any form accepted by `EntityUidJson`
61    parents: Vec<EntityUidJson>,
62    #[serde_as(as = "serde_with::MapPreventDuplicates<_,_>")]
63    #[serde(default)]
64    #[serde(skip_serializing_if = "HashMap::is_empty")]
65    #[cfg_attr(feature = "wasm", tsify(type = "Record<string, CedarValueJson>"))]
66    // the annotation covers duplicates in this `HashMap` itself, while the `JsonValueWithNoDuplicateKeys` covers duplicates in any records contained in tag values (including recursively)
67    tags: HashMap<SmolStr, JsonValueWithNoDuplicateKeys>,
68}
69
70/// Struct used to parse entities from JSON.
71#[derive(Debug, Clone)]
72pub struct EntityJsonParser<'e, 's, S = NoEntitiesSchema> {
73    /// See comments on [`EntityJsonParser::new()`] for the interpretation and
74    /// effects of this `schema` field.
75    ///
76    /// (Long doc comment on `EntityJsonParser::new()` is not repeated here, and
77    /// instead incorporated by reference, to avoid them becoming out of sync.)
78    schema: Option<&'s S>,
79
80    /// Extensions which are active for the JSON parsing.
81    extensions: &'e Extensions<'e>,
82
83    /// Whether to compute, enforce, or assume TC for entities parsed using this
84    /// parser.
85    tc_computation: TCComputation,
86}
87
88/// Schema information about a single entity can take one of these forms:
89#[derive(Debug)]
90enum EntitySchemaInfo<E> {
91    /// There is no schema, i.e. we're not doing schema-based parsing. We don't
92    /// have attribute type information in the schema for action entities, so
93    /// these are also parsed without schema-based parsing.
94    NoSchema,
95    /// The entity is a non-action, and here's the schema's information
96    /// about its type
97    NonAction(E),
98}
99
100impl<'e, 's, S> EntityJsonParser<'e, 's, S> {
101    /// Create a new `EntityJsonParser`.
102    ///
103    /// `schema` represents a source of `Action` entities, which will be added
104    /// to the entities parsed from JSON.
105    /// (If any `Action` entities are present in the JSON, and a `schema` is
106    /// also provided, each `Action` entity in the JSON must exactly match its
107    /// definition in the schema or an error is returned.)
108    ///
109    /// If a `schema` is present, this will also inform the parsing: for
110    /// instance, it will allow `__entity` and `__extn` escapes to be implicit.
111    ///
112    /// Finally, if a `schema` is present, the `EntityJsonParser` will ensure
113    /// that the produced entities fully conform to the `schema` -- for
114    /// instance, it will error if attributes have the wrong types (e.g., string
115    /// instead of integer), or if required attributes are missing or
116    /// superfluous attributes are provided.
117    ///
118    /// If you pass `TCComputation::AssumeAlreadyComputed`, then the caller is
119    /// responsible for ensuring that TC holds before calling this method.
120    pub fn new(
121        schema: Option<&'s S>,
122        extensions: &'e Extensions<'e>,
123        tc_computation: TCComputation,
124    ) -> Self {
125        Self {
126            schema,
127            extensions,
128            tc_computation,
129        }
130    }
131}
132
133impl<S: Schema> EntityJsonParser<'_, '_, S> {
134    /// Parse an entities JSON file (in [`&str`] form) into an [`Entities`] object.
135    ///
136    /// If the `EntityJsonParser` has a `schema`, this also adds `Action`
137    /// entities declared in the `schema`.
138    pub fn from_json_str(&self, json: &str) -> Result<Entities, EntitiesError> {
139        let ejsons: Vec<EntityJson> =
140            serde_json::from_str(json).map_err(JsonDeserializationError::from)?;
141        self.parse_ejsons(ejsons)
142    }
143
144    /// Parse an entities JSON file (in [`serde_json::Value`] form) into an [`Entities`] object.
145    ///
146    /// If the `EntityJsonParser` has a `schema`, this also adds `Action`
147    /// entities declared in the `schema`.
148    pub fn from_json_value(&self, json: serde_json::Value) -> Result<Entities, EntitiesError> {
149        let ejsons: Vec<EntityJson> =
150            serde_json::from_value(json).map_err(JsonDeserializationError::from)?;
151        self.parse_ejsons(ejsons)
152    }
153
154    /// Parse an entities JSON file (in [`std::io::Read`] form) into an [`Entities`] object.
155    ///
156    /// If the `EntityJsonParser` has a `schema`, this also adds `Action`
157    /// entities declared in the `schema`.
158    pub fn from_json_file(&self, json: impl std::io::Read) -> Result<Entities, EntitiesError> {
159        let ejsons: Vec<EntityJson> =
160            serde_json::from_reader(json).map_err(JsonDeserializationError::from)?;
161        self.parse_ejsons(ejsons)
162    }
163
164    /// Parse an entities JSON file (in [`&str`] form) into an iterator over [`Entity`]s.
165    ///
166    /// If the `EntityJsonParser` has a `schema`, this also adds `Action`
167    /// entities declared in the `schema`.
168    pub fn iter_from_json_str(
169        &self,
170        json: &str,
171    ) -> Result<impl Iterator<Item = Entity> + '_, EntitiesError> {
172        let ejsons: Vec<EntityJson> =
173            serde_json::from_str(json).map_err(JsonDeserializationError::from)?;
174        self.iter_ejson_to_iter_entity(ejsons)
175    }
176
177    /// Parse an entities JSON file (in [`serde_json::Value`] form) into an iterator over [`Entity`]s.
178    ///
179    /// If the `EntityJsonParser` has a `schema`, this also adds `Action`
180    /// entities declared in the `schema`.
181    pub fn iter_from_json_value(
182        &self,
183        json: serde_json::Value,
184    ) -> Result<impl Iterator<Item = Entity> + '_, EntitiesError> {
185        let ejsons: Vec<EntityJson> =
186            serde_json::from_value(json).map_err(JsonDeserializationError::from)?;
187        self.iter_ejson_to_iter_entity(ejsons)
188    }
189
190    /// Parse an entities JSON file (in [`std::io::Read`] form) into an iterator over [`Entity`]s.
191    ///
192    /// If the `EntityJsonParser` has a `schema`, this also adds `Action`
193    /// entities declared in the `schema`.
194    pub fn iter_from_json_file(
195        &self,
196        json: impl std::io::Read,
197    ) -> Result<impl Iterator<Item = Entity> + '_, EntitiesError> {
198        let ejsons: Vec<EntityJson> =
199            serde_json::from_reader(json).map_err(JsonDeserializationError::from)?;
200        self.iter_ejson_to_iter_entity(ejsons)
201    }
202
203    /// Internal function that converts an iterator over [`EntityJson`] into an
204    /// iterator over [`Entity`] and also adds any `Action` entities declared in
205    /// `self.schema`.
206    fn iter_ejson_to_iter_entity(
207        &self,
208        ejsons: impl IntoIterator<Item = EntityJson>,
209    ) -> Result<impl Iterator<Item = Entity> + '_, EntitiesError> {
210        let mut entities: Vec<Entity> = ejsons
211            .into_iter()
212            .map(|ejson| self.parse_ejson(ejson).map_err(EntitiesError::from))
213            .collect::<Result<_, _>>()?;
214        if let Some(schema) = &self.schema {
215            entities.extend(
216                schema
217                    .action_entities()
218                    .into_iter()
219                    .map(Arc::unwrap_or_clone),
220            );
221        }
222        Ok(entities.into_iter())
223    }
224
225    /// Parse a single entity from an in-memory JSON value
226    pub fn single_from_json_value(
227        &self,
228        value: serde_json::Value,
229    ) -> Result<Entity, EntitiesError> {
230        let ejson = serde_json::from_value(value).map_err(JsonDeserializationError::from)?;
231        self.single_from_ejson(ejson)
232    }
233
234    /// Parse a single entity from a JSON string
235    pub fn single_from_json_str(&self, src: impl AsRef<str>) -> Result<Entity, EntitiesError> {
236        let ejson = serde_json::from_str(src.as_ref()).map_err(JsonDeserializationError::from)?;
237        self.single_from_ejson(ejson)
238    }
239
240    /// Parse a single entity from a JSON reader
241    pub fn single_from_json_file(&self, r: impl Read) -> Result<Entity, EntitiesError> {
242        let ejson = serde_json::from_reader(r).map_err(JsonDeserializationError::from)?;
243        self.single_from_ejson(ejson)
244    }
245
246    fn single_from_ejson(&self, ejson: EntityJson) -> Result<Entity, EntitiesError> {
247        let entity = self.parse_ejson(ejson)?;
248        match self.schema {
249            None => Ok(entity),
250            Some(schema) => {
251                let checker = EntitySchemaConformanceChecker::new(schema, self.extensions);
252                checker.validate_entity(&entity)?;
253                Ok(entity)
254            }
255        }
256    }
257
258    /// Internal function that creates an [`Entities`] from a stream of [`EntityJson`].
259    ///
260    /// If the `EntityJsonParser` has a `schema`, this also adds `Action`
261    /// entities declared in the `schema`, and validates all the entities
262    /// against the schema.
263    fn parse_ejsons(
264        &self,
265        ejsons: impl IntoIterator<Item = EntityJson>,
266    ) -> Result<Entities, EntitiesError> {
267        let entities: Vec<Entity> = ejsons
268            .into_iter()
269            .map(|ejson| self.parse_ejson(ejson))
270            .collect::<Result<_, _>>()?;
271        Entities::from_entities(entities, self.schema, self.tc_computation, self.extensions)
272    }
273
274    /// Internal function that parses an `EntityJson` into an `Entity`.
275    ///
276    /// This function is not responsible for fully validating the `Entity`
277    /// against the `schema`; that happens on construction of an `Entities`
278    fn parse_ejson(&self, ejson: EntityJson) -> Result<Entity, JsonDeserializationError> {
279        let uid = ejson
280            .uid
281            .into_euid(|| JsonDeserializationErrorContext::EntityUid)?;
282        let etype = uid.entity_type();
283        let entity_schema_info = match &self.schema {
284            None => EntitySchemaInfo::NoSchema,
285            Some(schema) => {
286                if etype.is_action() {
287                    // Action entities do not have attribute type information in the schema.
288                    EntitySchemaInfo::NoSchema
289                } else {
290                    EntitySchemaInfo::NonAction(schema.entity_type(etype).ok_or_else(|| {
291                        let suggested_types = schema
292                            .entity_types_with_basename(&etype.name().basename())
293                            .collect();
294                        JsonDeserializationError::EntitySchemaConformance(
295                            UnexpectedEntityTypeError {
296                                uid: uid.clone(),
297                                suggested_types,
298                            }
299                            .into(),
300                        )
301                    })?)
302                }
303            }
304        };
305        let vparser = ValueParser::new(self.extensions);
306        let attrs: HashMap<SmolStr, RestrictedExpr> = ejson
307            .attrs
308            .into_iter()
309            .map(|(k, v)| match &entity_schema_info {
310                EntitySchemaInfo::NoSchema => Ok((
311                    k.clone(),
312                    vparser.val_into_restricted_expr(v.into(), None, || {
313                        JsonDeserializationErrorContext::EntityAttribute {
314                            uid: uid.clone(),
315                            attr: k.clone(),
316                        }
317                    })?,
318                )),
319                EntitySchemaInfo::NonAction(desc) => {
320                    // Depending on the expected type, we may parse the contents
321                    // of the attribute differently.
322                    let rexpr = match desc.attr_type(&k) {
323                        // `None` indicates the attribute shouldn't exist -- see
324                        // docs on the `attr_type()` trait method
325                        None => {
326                            if desc.open_attributes() {
327                                vparser.val_into_restricted_expr(v.into(), None, || {
328                                    JsonDeserializationErrorContext::EntityAttribute {
329                                        uid: uid.clone(),
330                                        attr: k.clone(),
331                                    }
332                                })?
333                            } else {
334                                return Err(JsonDeserializationError::EntitySchemaConformance(
335                                    EntitySchemaConformanceError::unexpected_entity_attr(
336                                        uid.clone(),
337                                        k,
338                                    ),
339                                ));
340                            }
341                        }
342                        Some(expected_ty) => vparser.val_into_restricted_expr(
343                            v.into(),
344                            Some(&expected_ty),
345                            || JsonDeserializationErrorContext::EntityAttribute {
346                                uid: uid.clone(),
347                                attr: k.clone(),
348                            },
349                        )?,
350                    };
351                    Ok((k, rexpr))
352                }
353            })
354            .collect::<Result<_, JsonDeserializationError>>()?;
355        let tags: HashMap<SmolStr, RestrictedExpr> = ejson
356            .tags
357            .into_iter()
358            .map(|(k, v)| match &entity_schema_info {
359                EntitySchemaInfo::NoSchema => Ok((
360                    k.clone(),
361                    vparser.val_into_restricted_expr(v.into(), None, || {
362                        JsonDeserializationErrorContext::EntityTag {
363                            uid: uid.clone(),
364                            tag: k.clone(),
365                        }
366                    })?,
367                )),
368                EntitySchemaInfo::NonAction(desc) => {
369                    // Depending on the expected type, we may parse the contents
370                    // of the tag differently.
371                    let rexpr = match desc.tag_type() {
372                        // `None` indicates no tags should exist -- see docs on
373                        // the `tag_type()` trait method
374                        None => {
375                            return Err(JsonDeserializationError::EntitySchemaConformance(
376                                EntitySchemaConformanceError::unexpected_entity_tag(uid.clone(), k),
377                            ));
378                        }
379                        Some(expected_ty) => vparser.val_into_restricted_expr(
380                            v.into(),
381                            Some(&expected_ty),
382                            || JsonDeserializationErrorContext::EntityTag {
383                                uid: uid.clone(),
384                                tag: k.clone(),
385                            },
386                        )?,
387                    };
388                    Ok((k, rexpr))
389                }
390            })
391            .collect::<Result<_, JsonDeserializationError>>()?;
392        let is_parent_allowed = |parent_euid: &EntityUID| {
393            // full validation isn't done in this function (see doc comments on
394            // this function), but we do need to do the following check which
395            // happens even when there is no schema
396            if etype.is_action() {
397                if parent_euid.is_action() {
398                    Ok(())
399                } else {
400                    Err(JsonDeserializationError::action_parent_is_not_action(
401                        uid.clone(),
402                        parent_euid.clone(),
403                    ))
404                }
405            } else {
406                Ok(()) // all parents are allowed
407            }
408        };
409        let parents = ejson
410            .parents
411            .into_iter()
412            .map(|parent| {
413                parent.into_euid(|| JsonDeserializationErrorContext::EntityParents {
414                    uid: uid.clone(),
415                })
416            })
417            .map(|res| {
418                res.and_then(|parent_euid| {
419                    is_parent_allowed(&parent_euid)?;
420                    Ok(parent_euid)
421                })
422            })
423            .collect::<Result<_, JsonDeserializationError>>()?;
424        Ok(Entity::new(
425            uid,
426            attrs,
427            HashSet::new(),
428            parents,
429            tags,
430            self.extensions,
431        )?)
432    }
433}
434
435impl EntityJson {
436    /// Convert an `Entity` into an `EntityJson`
437    ///
438    /// (for the reverse transformation, use `EntityJsonParser`)
439    pub fn from_entity(entity: &Entity) -> Result<Self, JsonSerializationError> {
440        let serialize_kpvalue = |(k, pvalue): (&SmolStr, &PartialValue)| -> Result<_, _> {
441            match pvalue {
442                PartialValue::Value(value) => {
443                    let cedarvaluejson = CedarValueJson::from_value(value.clone())?;
444                    Ok((k.clone(), serde_json::to_value(cedarvaluejson)?.into()))
445                }
446                PartialValue::Residual(expr) => match BorrowedRestrictedExpr::new(expr) {
447                    Ok(expr) => {
448                        let cedarvaluejson = CedarValueJson::from_expr(expr)?;
449                        Ok((k.clone(), serde_json::to_value(cedarvaluejson)?.into()))
450                    }
451                    Err(_) => Err(JsonSerializationError::residual(expr.clone())),
452                },
453            }
454        };
455        Ok(Self {
456            // for now, we encode `uid` and `parents` using an implied `__entity` escape
457            uid: EntityUidJson::ImplicitEntityEscape(TypeAndId::from(entity.uid())),
458            attrs: entity
459                .attrs()
460                .map(serialize_kpvalue)
461                .collect::<Result<_, JsonSerializationError>>()?,
462            parents: entity
463                .ancestors()
464                .map(|euid| EntityUidJson::ImplicitEntityEscape(TypeAndId::from(euid.clone())))
465                .collect(),
466            tags: entity
467                .tags()
468                .map(serialize_kpvalue)
469                .collect::<Result<_, JsonSerializationError>>()?,
470        })
471    }
472}
473
474// PANIC SAFETY unit test code
475#[allow(clippy::panic)]
476#[cfg(test)]
477mod test {
478    use super::*;
479    use cool_asserts::assert_matches;
480
481    #[test]
482    fn reject_inconsistent_duplicates() {
483        let json = serde_json::json!([
484            {
485                "uid" : {
486                    "type" : "User",
487                    "id" : "alice"
488                },
489                "attrs" : {},
490                "parents": []
491            },
492            {
493                "uid" : {
494                    "type" : "User",
495                    "id" : "alice"
496                },
497                "attrs" : {"location": "Greenland"},
498                "parents": []
499            }
500        ]);
501        let eparser: EntityJsonParser<'_, '_, NoEntitiesSchema> =
502            EntityJsonParser::new(None, Extensions::all_available(), TCComputation::ComputeNow);
503        let e = eparser.from_json_value(json);
504        let bad_euid: EntityUID = r#"User::"alice""#.parse().unwrap();
505        assert_matches!(e, Err(EntitiesError::Duplicate(euid)) => {
506          assert_eq!(&bad_euid, euid.euid(), r#"Returned euid should be User::"alice""#);
507        });
508    }
509
510    #[test]
511    fn simple() {
512        let test = serde_json::json!({
513            "uid" : { "type" : "A", "id" : "b" },
514            "attrs" : {},
515            "parents" : []
516        });
517        let x: Result<EntityJson, _> = serde_json::from_value(test);
518        x.unwrap();
519    }
520}