cedar_policy_validator/
schema_file_format.rs

1/*
2 * Copyright 2022-2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
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 cedar_policy_core::entities::JSONValue;
18use serde::{Deserialize, Serialize};
19use serde_with::serde_as;
20use smol_str::SmolStr;
21use std::collections::{BTreeMap, HashMap};
22
23use crate::Result;
24
25/// A SchemaFragment describe the types for a given instance of Cedar.
26/// SchemaFragments are composed of Entity Types and Action Types. The
27/// schema fragment is split into multiple namespace definitions, eac including
28/// a namespace name which is applied to all entity types (and the implicit
29/// `Action` entity type for all actions) in the schema.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(transparent)]
32pub struct SchemaFragment(
33    #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
34    pub  HashMap<SmolStr, NamespaceDefinition>,
35);
36
37impl SchemaFragment {
38    /// Create a `SchemaFragment` from a JSON value (which should be an object
39    /// of the appropriate shape).
40    pub fn from_json_value(json: serde_json::Value) -> Result<Self> {
41        serde_json::from_value(json).map_err(Into::into)
42    }
43
44    /// Create a `SchemaFragment` directly from a file.
45    pub fn from_file(file: impl std::io::Read) -> Result<Self> {
46        serde_json::from_reader(file).map_err(Into::into)
47    }
48}
49
50/// A single namespace definition from a SchemaFragment.
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52#[serde_as]
53#[serde(deny_unknown_fields)]
54#[doc(hidden)]
55pub struct NamespaceDefinition {
56    #[serde(default)]
57    #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
58    #[serde(rename = "commonTypes")]
59    pub common_types: HashMap<SmolStr, SchemaType>,
60    #[serde(rename = "entityTypes")]
61    #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
62    pub entity_types: HashMap<SmolStr, EntityType>,
63    #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
64    pub actions: HashMap<SmolStr, ActionType>,
65}
66
67impl NamespaceDefinition {
68    pub fn new(
69        entity_types: impl IntoIterator<Item = (SmolStr, EntityType)>,
70        actions: impl IntoIterator<Item = (SmolStr, ActionType)>,
71    ) -> Self {
72        Self {
73            common_types: HashMap::new(),
74            entity_types: entity_types.into_iter().collect(),
75            actions: actions.into_iter().collect(),
76        }
77    }
78}
79
80impl std::fmt::Display for NamespaceDefinition {
81    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
82        f.write_str(
83            &serde_json::to_string_pretty(&self).expect("failed to serialize NamespaceContents"),
84        )
85    }
86}
87
88/// Entity types describe the relationships in the entity store, including what
89/// entities can be members of groups of what types, and what attributes
90/// can/should be included on entities of each type.
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(deny_unknown_fields)]
93pub struct EntityType {
94    #[serde(default)]
95    #[serde(rename = "memberOfTypes")]
96    pub member_of_types: Vec<SmolStr>,
97    #[serde(default)]
98    pub shape: AttributesOrContext,
99}
100
101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
102#[serde(transparent)]
103pub struct AttributesOrContext(
104    // We use the usual `SchemaType` deserialization, but it will ultimately
105    // need to be a `Record` or type def which resolves to a `Record`.
106    pub SchemaType,
107);
108
109impl AttributesOrContext {
110    pub fn into_inner(self) -> SchemaType {
111        self.0
112    }
113}
114
115impl Default for AttributesOrContext {
116    fn default() -> Self {
117        Self(SchemaType::Type(SchemaTypeVariant::Record {
118            attributes: BTreeMap::new(),
119            additional_attributes: false,
120        }))
121    }
122}
123
124/// An action type describes a specific action entity.  It also describes what
125/// kinds of entities it can be used on.
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127#[serde(deny_unknown_fields)]
128pub struct ActionType {
129    /// This maps attribute names to
130    /// `cedar_policy_core::entities::json::jsonvalue::JSONValue` which is the
131    /// canonical representation of a cedar value as JSON.
132    #[serde(default)]
133    pub attributes: Option<HashMap<SmolStr, JSONValue>>,
134    #[serde(default)]
135    #[serde(rename = "appliesTo")]
136    pub applies_to: Option<ApplySpec>,
137    #[serde(default)]
138    #[serde(rename = "memberOf")]
139    pub member_of: Option<Vec<ActionEntityUID>>,
140}
141
142/// The apply spec specifies what principals and resources an action can be used
143/// with.  This specification can either be done through containing to entity
144/// types. The fields of this record are optional so that they can be omitted to
145/// declare that the apply spec for the principal or resource is undefined,
146/// meaning that the action can be applied to any principal or resource. This is
147/// different than providing an empty list because the empty list is interpreted
148/// as specifying that there are no principals or resources that an action
149/// applies to.
150#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151#[serde(deny_unknown_fields)]
152pub struct ApplySpec {
153    #[serde(default)]
154    #[serde(rename = "resourceTypes")]
155    pub resource_types: Option<Vec<SmolStr>>,
156    #[serde(default)]
157    #[serde(rename = "principalTypes")]
158    pub principal_types: Option<Vec<SmolStr>>,
159    #[serde(default)]
160    pub context: AttributesOrContext,
161}
162
163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
164#[serde(deny_unknown_fields)]
165pub struct ActionEntityUID {
166    pub id: SmolStr,
167
168    #[serde(rename = "type")]
169    #[serde(default)]
170    pub ty: Option<SmolStr>,
171}
172
173impl ActionEntityUID {
174    pub fn default_type(id: SmolStr) -> Self {
175        Self { id, ty: None }
176    }
177}
178
179impl std::fmt::Display for ActionEntityUID {
180    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181        if let Some(ty) = &self.ty {
182            write!(f, "{}::", ty)?
183        } else {
184            write!(f, "Action::")?
185        }
186        write!(f, "\"{}\"", self.id)
187    }
188}
189
190/// A restricted version of the `Type` enum containing only the types which are
191/// exposed to users.
192#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
193// This enum is `untagged` with these variants as a workaround to a serde
194// limitation. It is not possible to have the known variants on one enum, and
195// then, have catch-all variant for any unrecognized tag in the same enum that
196// captures the name of the unrecognized tag.
197#[serde(untagged)]
198pub enum SchemaType {
199    Type(SchemaTypeVariant),
200    TypeDef {
201        #[serde(rename = "type")]
202        type_name: SmolStr,
203    },
204}
205
206impl From<SchemaTypeVariant> for SchemaType {
207    fn from(variant: SchemaTypeVariant) -> Self {
208        Self::Type(variant)
209    }
210}
211
212#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
213#[serde(tag = "type")]
214#[serde(deny_unknown_fields)]
215pub enum SchemaTypeVariant {
216    String,
217    Long,
218    Boolean,
219    Set {
220        element: Box<SchemaType>,
221    },
222    Record {
223        #[serde(with = "serde_with::rust::maps_duplicate_key_is_error")]
224        attributes: BTreeMap<SmolStr, TypeOfAttribute>,
225        #[serde(rename = "additionalAttributes")]
226        #[serde(default = "additional_attributes_default")]
227        additional_attributes: bool,
228    },
229    Entity {
230        name: SmolStr,
231    },
232    Extension {
233        name: SmolStr,
234    },
235}
236
237// The possible tags for a SchemaType as written in a schema JSON document. Used
238// to forbid declaring a custom typedef with the same name as a builtin type.
239// This must be kept up to date with the variants for `SchemaTypeVariant` and
240// their actual serialization by serde. There is crate that looks like it could
241// do this automatically, but it returns an empty slice for the variants names
242// of `SchemaTypeVariant`.
243// https://docs.rs/serde-aux/latest/serde_aux/serde_introspection/fn.serde_introspect.html
244pub(crate) static SCHEMA_TYPE_VARIANT_TAGS: &[&str] = &[
245    "String",
246    "Long",
247    "Boolean",
248    "Set",
249    "Record",
250    "Entity",
251    "Extension",
252];
253
254impl SchemaType {
255    /// Is this `SchemaType` an extension type, or does it contain one
256    /// (recursively)? Returns `None` if this is a `TypeDef` because we can't
257    /// easily properly check the type of a typedef, accounting for namespaces,
258    /// without first converting to a `Type`.
259    pub fn is_extension(&self) -> Option<bool> {
260        match self {
261            Self::Type(SchemaTypeVariant::Extension { .. }) => Some(true),
262            Self::Type(SchemaTypeVariant::Set { element }) => element.is_extension(),
263            Self::Type(SchemaTypeVariant::Record { attributes, .. }) => {
264                attributes
265                    .values()
266                    .fold(Some(false), |a, e| match e.ty.is_extension() {
267                        Some(true) => Some(true),
268                        Some(false) => a,
269                        None => None,
270                    })
271            }
272            Self::Type(_) => Some(false),
273            Self::TypeDef { .. } => None,
274        }
275    }
276}
277
278#[cfg(fuzzing)]
279impl<'a> arbitrary::Arbitrary<'a> for SchemaType {
280    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<SchemaType> {
281        use cedar_policy_core::ast::Name;
282        use std::collections::HashSet;
283
284        Ok(SchemaType::Type(match u.int_in_range::<u8>(1..=8)? {
285            1 => SchemaTypeVariant::String,
286            2 => SchemaTypeVariant::Long,
287            3 => SchemaTypeVariant::Boolean,
288            4 => SchemaTypeVariant::Set {
289                element: Box::new(u.arbitrary()?),
290            },
291            5 => {
292                let attributes = {
293                    let attr_names: HashSet<String> = u.arbitrary()?;
294                    attr_names
295                        .into_iter()
296                        .map(|attr_name| Ok((attr_name.into(), u.arbitrary()?)))
297                        .collect::<arbitrary::Result<_>>()?
298                };
299                SchemaTypeVariant::Record {
300                    attributes,
301                    additional_attributes: u.arbitrary()?,
302                }
303            }
304            6 => {
305                let name: Name = u.arbitrary()?;
306                SchemaTypeVariant::Entity {
307                    name: name.to_string().into(),
308                }
309            }
310            7 => SchemaTypeVariant::Extension {
311                name: "ipaddr".into(),
312            },
313            8 => SchemaTypeVariant::Extension {
314                name: "decimal".into(),
315            },
316            n => panic!("bad index: {n}"),
317        }))
318    }
319    fn size_hint(_depth: usize) -> (usize, Option<usize>) {
320        (1, None) // Unfortunately, we probably can't be more precise than this
321    }
322}
323
324/// Used to describe the type of a record or entity attribute. It contains a the
325/// type of the attribute and whether the attribute is required. The type is
326/// flattened for serialization, so, in JSON format, this appears as a regular
327/// type with one extra property `required`.
328///
329/// Note that we can't add #[serde(deny_unknown_fields)] here because we are
330/// using #[serde(tag = "type")] in ty:SchemaType which is flattened here.
331/// The way serde(flatten) is implemented means it may be possible to access
332/// fields incorrectly if a struct contains two structs that are flattened
333/// (`<https://github.com/serde-rs/serde/issues/1547>`). This shouldn't apply to
334/// us as we're using flatten only once
335/// (`<https://github.com/serde-rs/serde/issues/1600>`). This should be ok because
336/// unknown fields for TypeOfAttribute should be passed to SchemaType where
337/// they will be denied (`<https://github.com/serde-rs/serde/issues/1600>`).
338#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Eq, PartialOrd, Ord)]
339#[cfg_attr(fuzzing, derive(arbitrary::Arbitrary))]
340pub struct TypeOfAttribute {
341    #[serde(flatten)]
342    pub ty: SchemaType,
343    #[serde(default = "record_attribute_required_default")]
344    pub required: bool,
345}
346
347/// Defines the default value for `additionalAttributes` on records and
348/// entities
349fn additional_attributes_default() -> bool {
350    false
351}
352
353/// Defines the default value for `required` on record and entity attributes.
354fn record_attribute_required_default() -> bool {
355    true
356}
357
358#[cfg(test)]
359mod test {
360    use super::*;
361
362    #[test]
363    fn test_entity_type_parser1() {
364        let user = r#"
365        {
366            "memberOfTypes" : ["UserGroup"]
367        }
368        "#;
369        let et = serde_json::from_str::<EntityType>(user).expect("Parse Error");
370        assert_eq!(et.member_of_types, vec!["UserGroup"]);
371        assert_eq!(
372            et.shape.into_inner(),
373            SchemaType::Type(SchemaTypeVariant::Record {
374                attributes: BTreeMap::new(),
375                additional_attributes: false
376            })
377        );
378    }
379
380    #[test]
381    fn test_entity_type_parser2() {
382        let src = r#"
383              { }
384        "#;
385        let et = serde_json::from_str::<EntityType>(src).expect("Parse Error");
386        assert_eq!(et.member_of_types.len(), 0);
387        assert_eq!(
388            et.shape.into_inner(),
389            SchemaType::Type(SchemaTypeVariant::Record {
390                attributes: BTreeMap::new(),
391                additional_attributes: false
392            })
393        );
394    }
395
396    #[test]
397    fn test_entity_type_parser3() {
398        let src = r#"
399        {
400            "memberOf" : ["UserGroup"],
401            "shape": {
402                "type": "Record",
403                "attributes": {
404                    "name": { "type": "String", "required": false},
405                    "name": { "type": "String", "required": true},
406                    "age": { "type": "Long", "required": false}
407                }
408            }
409        }
410        "#;
411        let et = serde_json::from_str::<EntityType>(src);
412        match et {
413            Ok(_) => panic!("serde_json parsing should have failed"),
414            Err(e) => {
415                assert_eq!(e.classify(), serde_json::error::Category::Data);
416            }
417        }
418    }
419
420    #[test]
421    fn test_action_type_parser1() {
422        let src = r#"
423              {
424                "appliesTo" : {
425                  "resourceTypes": ["Album"],
426                  "principalTypes": ["User"]
427                },
428                "memberOf": [{"id": "readWrite"}]
429              }
430        "#;
431        let at: ActionType = serde_json::from_str(src).expect("Parse Error");
432        let spec = ApplySpec {
433            resource_types: Some(vec!["Album".into()]),
434            principal_types: Some(vec!["User".into()]),
435            context: AttributesOrContext::default(),
436        };
437        assert_eq!(at.applies_to, Some(spec));
438        assert_eq!(
439            at.member_of,
440            Some(vec![ActionEntityUID {
441                ty: None,
442                id: "readWrite".into()
443            }])
444        );
445    }
446
447    #[test]
448    fn test_action_type_parser2() {
449        let src = r#"
450              { }
451        "#;
452        let at: ActionType = serde_json::from_str(src).expect("Parse Error");
453        assert_eq!(at.applies_to, None);
454        assert!(at.member_of.is_none());
455    }
456
457    #[test]
458    fn test_schema_file_parser() {
459        let src = serde_json::json!(
460        {
461            "entityTypes": {
462
463              "User": {
464                "memberOfTypes": ["UserGroup"]
465              },
466              "Photo": {
467                "memberOfTypes": ["Album", "Account"]
468              },
469
470              "Album": {
471                "memberOfTypes": ["Album", "Account"]
472              },
473              "Account": { },
474              "UserGroup": { }
475           },
476
477           "actions": {
478              "readOnly": { },
479              "readWrite": { },
480              "createAlbum": {
481                "appliesTo" : {
482                  "resourceTypes": ["Account", "Album"],
483                  "principalTypes": ["User"]
484                },
485                "memberOf": [{"id": "readWrite"}]
486              },
487              "addPhotoToAlbum": {
488                "appliesTo" : {
489                  "resourceTypes": ["Album"],
490                  "principalTypes": ["User"]
491                },
492                "memberOf": [{"id": "readWrite"}]
493              },
494              "viewPhoto": {
495                "appliesTo" : {
496                  "resourceTypes": ["Photo"],
497                  "principalTypes": ["User"]
498                },
499                "memberOf": [{"id": "readOnly"}, {"id": "readWrite"}]
500              },
501              "viewComments": {
502                "appliesTo" : {
503                  "resourceTypes": ["Photo"],
504                  "principalTypes": ["User"]
505                },
506                "memberOf": [{"id": "readOnly"}, {"id": "readWrite"}]
507              }
508            }
509          });
510        let schema_file: NamespaceDefinition = serde_json::from_value(src).expect("Parse Error");
511
512        assert_eq!(schema_file.entity_types.len(), 5);
513        assert_eq!(schema_file.actions.len(), 6);
514    }
515
516    #[test]
517    fn test_parse_namespaces() {
518        let src = r#"
519        {
520            "foo::foo::bar::baz": {
521                "entityTypes": {},
522                "actions": {}
523            }
524        }"#;
525        let schema: SchemaFragment = serde_json::from_str(src).expect("Parse Error");
526        let (namespace, _descriptor) = schema.0.into_iter().next().unwrap();
527        assert_eq!(namespace, "foo::foo::bar::baz".to_string());
528    }
529
530    #[test]
531    #[should_panic]
532    fn test_schema_file_with_misspelled_required() {
533        let src = r#"
534        {
535            "entityTypes": {
536                "User": {
537                    "memberOf": [ "Group" ],
538                    "shape": {
539                        "type": "Record",
540                        "additionalAttributess": false,
541                        "attributes": {
542                            "name": { "type": "String", "required": true},
543                            "age": { "type": "Long", "required": true},
544                            "favorite": { "type": "Entity", "name": "Photo", "requiredddddd": false}
545                        },
546                        "required": false
547                    }
548                }
549            },
550            "actions": []
551        }
552        "#;
553        let schema: NamespaceDefinition = serde_json::from_str(src).expect("Expected valid schema");
554        println!("{:#?}", schema);
555    }
556
557    #[test]
558    #[should_panic]
559    fn test_schema_file_with_misspelled_attribute() {
560        let src = r#"
561        {
562            "entityTypes": [
563                "User": {
564                    "memberOf": [ "Group" ],
565                    "shape": {
566                        "type": "Record",
567                        "additionalAttributess": false,
568                        "attributes": {
569                            "name": { "type": "String", "required": true},
570                            "age": { "type": "Long", "required": true},
571                            "favorite": { "type": "Entity", "nameeeeee": "Photo", "required": false}
572                        },
573                        "required": false
574                    }
575                }
576            ],
577            "actions": []
578        }
579        "#;
580        let schema: NamespaceDefinition = serde_json::from_str(src).expect("Expected valid schema");
581        println!("{:#?}", schema);
582    }
583
584    #[test]
585    #[should_panic]
586    fn test_schema_file_with_extra_attribute() {
587        let src = r#"
588        {
589            "entityTypes": [
590                "User": {
591                    "memberOf": [ "Group" ],
592                    "shape": {
593                        "type": "Record",
594                        "additionalAttributess": false,
595                        "attributes": {
596                            "name": { "type": "String", "required": true},
597                            "age": { "type": "Long", "required": true},
598                            "favorite": { "type": "Entity", "name": "Photo", "required": false, "extra": "Should not exist"}
599                        },
600                        "required": false
601                    }
602                }
603            ],
604            "actions": []
605        }
606        "#;
607        let schema: NamespaceDefinition = serde_json::from_str(src).expect("Expected valid schema");
608        println!("{:#?}", schema);
609    }
610}