1use 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#[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 pub fn from_json_value(json: serde_json::Value) -> Result<Self> {
41 serde_json::from_value(json).map_err(Into::into)
42 }
43
44 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#[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#[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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127#[serde(deny_unknown_fields)]
128pub struct ActionType {
129 #[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#[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#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
193#[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
237pub(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 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) }
322}
323
324#[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
347fn additional_attributes_default() -> bool {
350 false
351}
352
353fn 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}