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