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