cedar_policy_validator/cedar_schema/
to_json_schema.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
17//! Convert a schema into the JSON format
18
19use std::collections::HashMap;
20
21use cedar_policy_core::{
22    ast::{Id, Name, UnreservedId},
23    extensions::Extensions,
24    parser::{Loc, Node},
25};
26use itertools::Either;
27use nonempty::NonEmpty;
28use smol_str::{SmolStr, ToSmolStr};
29use std::collections::hash_map::Entry;
30
31use super::{
32    ast::{
33        ActionDecl, AppDecl, AttrDecl, Decl, Declaration, EntityDecl, Namespace, PRAppDecl, Path,
34        QualName, Schema, Type, TypeDecl, BUILTIN_TYPES, PR,
35    },
36    err::{schema_warnings, SchemaWarning, ToJsonSchemaError, ToJsonSchemaErrors},
37};
38use crate::{cedar_schema, json_schema, RawName};
39
40impl From<cedar_schema::Path> for RawName {
41    fn from(p: cedar_schema::Path) -> Self {
42        RawName::from_name(p.into())
43    }
44}
45
46/// Convert a schema AST into the JSON representation.
47/// This will let you subsequently decode that into the Validator AST for Schemas ([`crate::ValidatorSchema`]).
48/// On success, this function returns a tuple containing:
49///     * The `json_schema::Fragment`
50///     * An iterator of warnings that were generated
51///
52/// TODO(#1085): These warnings should be generated later in the process, such
53/// that we apply the same checks to JSON and Cedar schemas
54pub fn cedar_schema_to_json_schema(
55    schema: Schema,
56    extensions: &Extensions<'_>,
57) -> Result<
58    (
59        json_schema::Fragment<RawName>,
60        impl Iterator<Item = SchemaWarning>,
61    ),
62    ToJsonSchemaErrors,
63> {
64    // combine all of the declarations in unqualified (empty) namespaces into a
65    // single unqualified namespace
66    //
67    // TODO(#1086): If we want to allow reopening a namespace within the same
68    // (Cedar) schema fragment, then in this step we would also need to combine
69    // namespaces with matching non-empty names, so that all definitions from
70    // that namespace make it into the JSON schema structure under that
71    // namespace's key.
72    let (qualified_namespaces, unqualified_namespace) =
73        split_unqualified_namespace(schema.into_iter().map(|n| n.node));
74    // Create a single iterator for all namespaces
75    let all_namespaces = qualified_namespaces
76        .chain(unqualified_namespace)
77        .collect::<Vec<_>>();
78
79    let names = build_namespace_bindings(all_namespaces.iter())?;
80    let warnings = compute_namespace_warnings(&names, extensions);
81    let fragment = collect_all_errors(all_namespaces.into_iter().map(convert_namespace))?.collect();
82    Ok((
83        json_schema::Fragment(fragment),
84        warnings.collect::<Vec<_>>().into_iter(),
85    ))
86}
87
88/// Is the given [`Id`] the name of a valid extension type, given the currently active [`Extensions`]
89fn is_valid_ext_type(ty: &Id, extensions: &Extensions<'_>) -> bool {
90    extensions
91        .ext_types()
92        .filter(|ext_ty| ext_ty.as_ref().is_unqualified()) // if there are any qualified extension type names, we don't care, because we're looking for an unqualified name `ty`
93        .any(|ext_ty| ty == ext_ty.basename_as_ref())
94}
95
96/// Convert a `Type` into the JSON representation of the type.
97pub fn cedar_type_to_json_type(ty: Node<Type>) -> json_schema::Type<RawName> {
98    match ty.node {
99        Type::Set(t) => json_schema::Type::Type(json_schema::TypeVariant::Set {
100            element: Box::new(cedar_type_to_json_type(*t)),
101        }),
102        Type::Ident(p) => json_schema::Type::Type(json_schema::TypeVariant::EntityOrCommon {
103            type_name: RawName::from(p),
104        }),
105        Type::Record(fields) => {
106            json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
107                attributes: fields
108                    .into_iter()
109                    .map(|field| convert_attr_decl(field.node))
110                    .collect(),
111                additional_attributes: false,
112            }))
113        }
114    }
115}
116
117// Split namespaces into two groups: named namespaces and the implicit unqualified namespace
118// The rhs of the tuple will be [`None`] if there are no items in the unqualified namespace.
119fn split_unqualified_namespace(
120    namespaces: impl IntoIterator<Item = Namespace>,
121) -> (impl Iterator<Item = Namespace>, Option<Namespace>) {
122    // First split every namespace into those with explicit names and those without
123    let (qualified, unqualified): (Vec<_>, Vec<_>) =
124        namespaces.into_iter().partition(|n| n.name.is_some());
125
126    // Now combine all the decls in namespaces without names into one unqualified namespace
127    let mut unqualified_decls = vec![];
128    for mut unqualified_namespace in unqualified.into_iter() {
129        unqualified_decls.append(&mut unqualified_namespace.decls);
130    }
131
132    if unqualified_decls.is_empty() {
133        (qualified.into_iter(), None)
134    } else {
135        let unqual = Namespace {
136            name: None,
137            decls: unqualified_decls,
138        };
139        (qualified.into_iter(), Some(unqual))
140    }
141}
142
143/// Converts a CST namespace to a JSON namespace
144fn convert_namespace(
145    namespace: Namespace,
146) -> Result<(Option<Name>, json_schema::NamespaceDefinition<RawName>), ToJsonSchemaErrors> {
147    let ns_name = namespace
148        .name
149        .clone()
150        .map(|p| {
151            let internal_name = RawName::from(p.node).qualify_with(None); // namespace names are always written already-fully-qualified in the Cedar schema syntax
152            Name::try_from(internal_name)
153                .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), p.loc))
154        })
155        .transpose()?;
156    let def = namespace.try_into()?;
157    Ok((ns_name, def))
158}
159
160impl TryFrom<Namespace> for json_schema::NamespaceDefinition<RawName> {
161    type Error = ToJsonSchemaErrors;
162
163    fn try_from(n: Namespace) -> Result<json_schema::NamespaceDefinition<RawName>, Self::Error> {
164        // Partition the decls into entities, actions, and common types
165        let (entity_types, action, common_types) = into_partition_decls(n.decls);
166
167        // Convert entity type decls, collecting all errors
168        let entity_types = collect_all_errors(entity_types.into_iter().map(convert_entity_decl))?
169            .flatten()
170            .collect();
171
172        // Convert action decls, collecting all errors
173        let actions = collect_all_errors(action.into_iter().map(convert_action_decl))?
174            .flatten()
175            .collect();
176
177        // Convert common type decls
178        let common_types = common_types
179            .into_iter()
180            .map(|decl| {
181                let name_loc = decl.name.loc.clone();
182                let id = UnreservedId::try_from(decl.name.node)
183                    .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), name_loc.clone()))?;
184                let ctid = json_schema::CommonTypeId::new(id)
185                    .map_err(|e| ToJsonSchemaError::reserved_keyword(e.id, name_loc))?;
186                Ok((ctid, cedar_type_to_json_type(decl.def)))
187            })
188            .collect::<Result<_, ToJsonSchemaError>>()?;
189
190        Ok(json_schema::NamespaceDefinition {
191            common_types,
192            entity_types,
193            actions,
194        })
195    }
196}
197
198/// Converts action type decls
199fn convert_action_decl(
200    a: ActionDecl,
201) -> Result<impl Iterator<Item = (SmolStr, json_schema::ActionType<RawName>)>, ToJsonSchemaErrors> {
202    let ActionDecl {
203        names,
204        parents,
205        app_decls,
206    } = a;
207    // Create the internal type from the 'applies_to' clause and 'member_of'
208    let applies_to = app_decls
209        .map(|decls| convert_app_decls(&names.first().node, &names.first().loc, decls))
210        .transpose()?
211        .unwrap_or_else(|| json_schema::ApplySpec {
212            resource_types: vec![],
213            principal_types: vec![],
214            context: json_schema::AttributesOrContext::default(),
215        });
216    let member_of = parents.map(|parents| parents.into_iter().map(convert_qual_name).collect());
217    let ty = json_schema::ActionType {
218        attributes: None, // Action attributes are currently unsupported in the Cedar schema format
219        applies_to: Some(applies_to),
220        member_of,
221    };
222    // Then map that type across all of the bound names
223    Ok(names.into_iter().map(move |name| (name.node, ty.clone())))
224}
225
226fn convert_qual_name(qn: Node<QualName>) -> json_schema::ActionEntityUID<RawName> {
227    json_schema::ActionEntityUID::new(qn.node.path.map(Into::into), qn.node.eid)
228}
229
230/// Convert the applies to decls
231/// # Arguments
232/// * `name` - The (first) name of the action being declared
233/// * `name_loc` - The location of that first name
234fn convert_app_decls(
235    name: &SmolStr,
236    name_loc: &Loc,
237    decls: Node<NonEmpty<Node<AppDecl>>>,
238) -> Result<json_schema::ApplySpec<RawName>, ToJsonSchemaErrors> {
239    // Split AppDecl's into context/principal/resource decls
240    let (decls, _) = decls.into_inner();
241    let mut principal_types: Option<Node<Vec<RawName>>> = None;
242    let mut resource_types: Option<Node<Vec<RawName>>> = None;
243    let mut context: Option<Node<json_schema::AttributesOrContext<RawName>>> = None;
244
245    for decl in decls {
246        match decl {
247            Node {
248                node: AppDecl::Context(context_decl),
249                loc,
250            } => match context {
251                Some(existing_context) => {
252                    return Err(ToJsonSchemaError::duplicate_context(
253                        name.clone(),
254                        existing_context.loc,
255                        loc,
256                    )
257                    .into());
258                }
259                None => {
260                    context = Some(Node::with_source_loc(
261                        convert_context_decl(context_decl),
262                        loc,
263                    ));
264                }
265            },
266            Node {
267                node:
268                    AppDecl::PR(PRAppDecl {
269                        kind:
270                            Node {
271                                node: PR::Principal,
272                                ..
273                            },
274                        entity_tys,
275                    }),
276                loc,
277            } => match principal_types {
278                Some(existing_tys) => {
279                    return Err(ToJsonSchemaError::duplicate_principal(
280                        name.clone(),
281                        existing_tys.loc,
282                        loc,
283                    )
284                    .into());
285                }
286                None => {
287                    principal_types = Some(Node::with_source_loc(
288                        entity_tys.iter().map(|n| n.clone().into()).collect(),
289                        loc,
290                    ))
291                }
292            },
293            Node {
294                node:
295                    AppDecl::PR(PRAppDecl {
296                        kind:
297                            Node {
298                                node: PR::Resource, ..
299                            },
300                        entity_tys,
301                    }),
302                loc,
303            } => match resource_types {
304                Some(existing_tys) => {
305                    return Err(ToJsonSchemaError::duplicate_resource(
306                        name.clone(),
307                        existing_tys.loc,
308                        loc,
309                    )
310                    .into());
311                }
312                None => {
313                    resource_types = Some(Node::with_source_loc(
314                        entity_tys.iter().map(|n| n.clone().into()).collect(),
315                        loc,
316                    ))
317                }
318            },
319        }
320    }
321    Ok(json_schema::ApplySpec {
322        resource_types: resource_types.map(|node| node.node).ok_or(
323            ToJsonSchemaError::no_resource(name.clone(), name_loc.clone()),
324        )?,
325        principal_types: principal_types.map(|node| node.node).ok_or(
326            ToJsonSchemaError::no_principal(name.clone(), name_loc.clone()),
327        )?,
328        context: context.map(|c| c.node).unwrap_or_default(),
329    })
330}
331
332fn convert_id(node: Node<Id>) -> Result<UnreservedId, ToJsonSchemaError> {
333    UnreservedId::try_from(node.node)
334        .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), node.loc))
335}
336
337/// Convert Entity declarations
338fn convert_entity_decl(
339    e: EntityDecl,
340) -> Result<
341    impl Iterator<Item = (UnreservedId, json_schema::EntityType<RawName>)>,
342    ToJsonSchemaErrors,
343> {
344    // First build up the defined entity type
345    let etype = json_schema::EntityType {
346        member_of_types: e.member_of_types.into_iter().map(RawName::from).collect(),
347        shape: convert_attr_decls(e.attrs),
348        tags: e.tags.map(cedar_type_to_json_type),
349    };
350
351    // Then map over all of the bound names
352    collect_all_errors(
353        e.names
354            .into_iter()
355            .map(move |name| -> Result<_, ToJsonSchemaErrors> {
356                Ok((convert_id(name)?, etype.clone()))
357            }),
358    )
359}
360
361/// Create a [`json_schema::AttributesOrContext`] from a series of `AttrDecl`s
362fn convert_attr_decls(
363    attrs: impl IntoIterator<Item = Node<AttrDecl>>,
364) -> json_schema::AttributesOrContext<RawName> {
365    json_schema::RecordType {
366        attributes: attrs
367            .into_iter()
368            .map(|attr| convert_attr_decl(attr.node))
369            .collect(),
370        additional_attributes: false,
371    }
372    .into()
373}
374
375/// Create a context decl
376fn convert_context_decl(
377    decl: Either<Path, Vec<Node<AttrDecl>>>,
378) -> json_schema::AttributesOrContext<RawName> {
379    json_schema::AttributesOrContext(match decl {
380        Either::Left(p) => json_schema::Type::CommonTypeRef {
381            type_name: p.into(),
382        },
383        Either::Right(attrs) => {
384            json_schema::Type::Type(json_schema::TypeVariant::Record(json_schema::RecordType {
385                attributes: attrs
386                    .into_iter()
387                    .map(|attr| convert_attr_decl(attr.node))
388                    .collect(),
389                additional_attributes: false,
390            }))
391        }
392    })
393}
394
395/// Convert an attribute type from an `AttrDecl`
396fn convert_attr_decl(attr: AttrDecl) -> (SmolStr, json_schema::TypeOfAttribute<RawName>) {
397    (
398        attr.name.node,
399        json_schema::TypeOfAttribute {
400            ty: cedar_type_to_json_type(attr.ty),
401            required: attr.required,
402        },
403    )
404}
405
406/// Takes a collection of results returning multiple errors
407/// Behaves similarly to `::collect()` over results, except instead of failing
408/// on the first error, keeps going to ensure all of the errors are accumulated
409fn collect_all_errors<A, E>(
410    iter: impl IntoIterator<Item = Result<A, E>>,
411) -> Result<impl Iterator<Item = A>, ToJsonSchemaErrors>
412where
413    E: IntoIterator<Item = ToJsonSchemaError>,
414{
415    let mut answers = vec![];
416    let mut errs = vec![];
417    for r in iter.into_iter() {
418        match r {
419            Ok(a) => {
420                answers.push(a);
421            }
422            Err(e) => {
423                let mut v = e.into_iter().collect::<Vec<_>>();
424                errs.append(&mut v)
425            }
426        }
427    }
428    match NonEmpty::collect(errs) {
429        None => Ok(answers.into_iter()),
430        Some(errs) => Err(ToJsonSchemaErrors::new(errs)),
431    }
432}
433
434#[derive(Default)]
435struct NamespaceRecord {
436    entities: HashMap<Id, Node<()>>,
437    common_types: HashMap<Id, Node<()>>,
438    loc: Option<Loc>,
439}
440
441impl NamespaceRecord {
442    fn new(namespace: &Namespace) -> Result<(Option<Name>, Self), ToJsonSchemaErrors> {
443        let ns = namespace
444            .name
445            .clone()
446            .map(|n| {
447                let internal_name = RawName::from(n.node).qualify_with(None); // namespace names are already fully-qualified
448                Name::try_from(internal_name)
449                    .map_err(|e| ToJsonSchemaError::reserved_name(e.name(), n.loc))
450            })
451            .transpose()?;
452        let (entities, actions, types) = partition_decls(&namespace.decls);
453
454        let entities = collect_decls(
455            entities
456                .into_iter()
457                .flat_map(|decl| decl.names.clone())
458                .map(extract_name),
459        )?;
460        // Ensure no duplicate actions
461        collect_decls(
462            actions
463                .into_iter()
464                .flat_map(ActionDecl::names)
465                .map(extract_name),
466        )?;
467        let common_types = collect_decls(
468            types
469                .into_iter()
470                .flat_map(|decl| std::iter::once(decl.name.clone()))
471                .map(extract_name),
472        )?;
473
474        let record = NamespaceRecord {
475            entities,
476            common_types,
477            loc: namespace.name.as_ref().map(|n| n.loc.clone()),
478        };
479
480        Ok((ns, record))
481    }
482}
483
484fn collect_decls<N>(
485    i: impl Iterator<Item = (N, Node<()>)>,
486) -> Result<HashMap<N, Node<()>>, ToJsonSchemaErrors>
487where
488    N: std::cmp::Eq + std::hash::Hash + Clone + ToSmolStr,
489{
490    let mut map: HashMap<N, Node<()>> = HashMap::new();
491    for (key, node) in i {
492        match map.entry(key.clone()) {
493            Entry::Occupied(entry) => Err(ToJsonSchemaError::duplicate_decls(
494                key,
495                entry.get().loc.clone(),
496                node.loc,
497            )),
498            Entry::Vacant(entry) => {
499                entry.insert(node);
500                Ok(())
501            }
502        }?;
503    }
504    Ok(map)
505}
506
507fn compute_namespace_warnings<'a>(
508    fragment: &'a HashMap<Option<Name>, NamespaceRecord>,
509    extensions: &'a Extensions<'a>,
510) -> impl Iterator<Item = SchemaWarning> + 'a {
511    fragment
512        .values()
513        .flat_map(move |nr| make_warning_for_shadowing(nr, extensions))
514}
515
516fn make_warning_for_shadowing<'a>(
517    n: &'a NamespaceRecord,
518    extensions: &'a Extensions<'a>,
519) -> impl Iterator<Item = SchemaWarning> + 'a {
520    let mut warnings = vec![];
521    for (common_name, common_src_node) in n.common_types.iter() {
522        // Check if it shadows a entity name in the same namespace
523        if let Some(entity_src_node) = n.entities.get(common_name) {
524            let warning = schema_warnings::ShadowsEntityWarning {
525                name: common_name.to_smolstr(),
526                entity_loc: entity_src_node.loc.clone(),
527                common_loc: common_src_node.loc.clone(),
528            }
529            .into();
530            warnings.push(warning);
531        }
532        // Check if it shadows a builtin
533        if let Some(warning) = shadows_builtin(common_name, common_src_node, extensions) {
534            warnings.push(warning);
535        }
536    }
537    let entity_shadows = n
538        .entities
539        .iter()
540        .filter_map(move |(name, node)| shadows_builtin(name, node, extensions));
541    warnings.into_iter().chain(entity_shadows)
542}
543
544fn extract_name<N: Clone>(n: Node<N>) -> (N, Node<()>) {
545    (n.node.clone(), n.map(|_| ()))
546}
547
548fn shadows_builtin(
549    name: &Id,
550    node: &Node<()>,
551    extensions: &Extensions<'_>,
552) -> Option<SchemaWarning> {
553    if is_valid_ext_type(name, extensions) || BUILTIN_TYPES.contains(&name.as_ref()) {
554        Some(
555            schema_warnings::ShadowsBuiltinWarning {
556                name: name.to_smolstr(),
557                loc: node.loc.clone(),
558            }
559            .into(),
560        )
561    } else {
562        None
563    }
564}
565
566// Essentially index `NamespaceRecord`s by the namespace
567fn build_namespace_bindings<'a>(
568    namespaces: impl Iterator<Item = &'a Namespace>,
569) -> Result<HashMap<Option<Name>, NamespaceRecord>, ToJsonSchemaErrors> {
570    let mut map = HashMap::new();
571    for (name, record) in collect_all_errors(namespaces.map(NamespaceRecord::new))? {
572        update_namespace_record(&mut map, name, record)?;
573    }
574    Ok(map)
575}
576
577fn update_namespace_record(
578    map: &mut HashMap<Option<Name>, NamespaceRecord>,
579    name: Option<Name>,
580    record: NamespaceRecord,
581) -> Result<(), ToJsonSchemaErrors> {
582    match map.entry(name.clone()) {
583        Entry::Occupied(entry) => Err(ToJsonSchemaError::duplicate_namespace(
584            name.map_or("".into(), |n| n.to_smolstr()),
585            record.loc,
586            entry.get().loc.clone(),
587        )
588        .into()),
589        Entry::Vacant(entry) => {
590            entry.insert(record);
591            Ok(())
592        }
593    }
594}
595
596fn partition_decls(
597    decls: &[Node<Declaration>],
598) -> (Vec<&EntityDecl>, Vec<&ActionDecl>, Vec<&TypeDecl>) {
599    let mut entities = vec![];
600    let mut actions = vec![];
601    let mut types = vec![];
602
603    for decl in decls.iter() {
604        match &decl.node {
605            Declaration::Entity(e) => entities.push(e),
606            Declaration::Action(a) => actions.push(a),
607            Declaration::Type(t) => types.push(t),
608        }
609    }
610
611    (entities, actions, types)
612}
613
614fn into_partition_decls(
615    decls: Vec<Node<Declaration>>,
616) -> (Vec<EntityDecl>, Vec<ActionDecl>, Vec<TypeDecl>) {
617    let mut entities = vec![];
618    let mut actions = vec![];
619    let mut types = vec![];
620
621    for decl in decls.into_iter() {
622        match decl.node {
623            Declaration::Entity(e) => entities.push(e),
624            Declaration::Action(a) => actions.push(a),
625            Declaration::Type(t) => types.push(t),
626        }
627    }
628
629    (entities, actions, types)
630}