cedar_policy_validator/cedar_schema/
fmt.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//! `Display` implementations for formatting a [`json_schema::Fragment`] in the
18//! Cedar schema syntax
19
20use std::{collections::HashSet, fmt::Display};
21
22use itertools::Itertools;
23use miette::Diagnostic;
24use nonempty::NonEmpty;
25use smol_str::{SmolStr, ToSmolStr};
26use thiserror::Error;
27
28use crate::{json_schema, RawName};
29
30impl<N: Display> Display for json_schema::Fragment<N> {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        for (ns, def) in &self.0 {
33            match ns {
34                None => write!(f, "{def}")?,
35                Some(ns) => write!(f, "namespace {ns} {{\n{def}}}\n")?,
36            }
37        }
38        Ok(())
39    }
40}
41
42impl<N: Display> Display for json_schema::NamespaceDefinition<N> {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        for (n, ty) in &self.common_types {
45            writeln!(f, "type {n} = {ty};")?
46        }
47        for (n, ty) in &self.entity_types {
48            writeln!(f, "entity {n}{ty};")?
49        }
50        for (n, a) in &self.actions {
51            writeln!(f, "action \"{}\"{a};", n.escape_debug())?
52        }
53        Ok(())
54    }
55}
56
57impl<N: Display> Display for json_schema::Type<N> {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        match self {
60            json_schema::Type::Type(ty) => match ty {
61                json_schema::TypeVariant::Boolean => write!(f, "__cedar::Bool"),
62                json_schema::TypeVariant::Entity { name } => write!(f, "{name}"),
63                json_schema::TypeVariant::EntityOrCommon { type_name } => {
64                    write!(f, "{type_name}")
65                }
66                json_schema::TypeVariant::Extension { name } => write!(f, "__cedar::{name}"),
67                json_schema::TypeVariant::Long => write!(f, "__cedar::Long"),
68                json_schema::TypeVariant::Record(rty) => write!(f, "{rty}"),
69                json_schema::TypeVariant::Set { element } => write!(f, "Set < {element} >"),
70                json_schema::TypeVariant::String => write!(f, "__cedar::String"),
71            },
72            json_schema::Type::CommonTypeRef { type_name } => write!(f, "{type_name}"),
73        }
74    }
75}
76
77impl<N: Display> Display for json_schema::RecordType<N> {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        write!(f, "{{")?;
80        for (i, (n, ty)) in self.attributes.iter().enumerate() {
81            write!(
82                f,
83                "\"{}\"{}: {}",
84                n.escape_debug(),
85                if ty.required { "" } else { "?" },
86                ty.ty
87            )?;
88            if i < (self.attributes.len() - 1) {
89                write!(f, ", ")?;
90            }
91        }
92        write!(f, "}}")?;
93        Ok(())
94    }
95}
96
97/// Create a non-empty with borrowed contents from a slice
98fn non_empty_slice<T>(v: &[T]) -> Option<NonEmpty<&T>> {
99    let vs: Vec<&T> = v.iter().collect();
100    NonEmpty::from_vec(vs)
101}
102
103fn fmt_vec<T: Display>(f: &mut std::fmt::Formatter<'_>, ets: NonEmpty<T>) -> std::fmt::Result {
104    let contents = ets.iter().map(T::to_string).join(", ");
105    write!(f, "[{contents}]")
106}
107
108impl<N: Display> Display for json_schema::EntityType<N> {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        if let Some(non_empty) = non_empty_slice(&self.member_of_types) {
111            write!(f, " in ")?;
112            fmt_vec(f, non_empty)?;
113        }
114
115        let ty = &self.shape;
116        // Don't print `= { }`
117        if !ty.is_empty_record() {
118            write!(f, " = {ty}")?;
119        }
120
121        if let Some(tags) = &self.tags {
122            write!(f, " tags {tags}")?;
123        }
124
125        Ok(())
126    }
127}
128
129impl<N: Display> Display for json_schema::ActionType<N> {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        if let Some(parents) = self
132            .member_of
133            .as_ref()
134            .and_then(|refs| non_empty_slice(refs.as_slice()))
135        {
136            write!(f, " in ")?;
137            fmt_vec(f, parents)?;
138        }
139        if let Some(spec) = &self.applies_to {
140            match (
141                non_empty_slice(spec.principal_types.as_slice()),
142                non_empty_slice(spec.resource_types.as_slice()),
143            ) {
144                // One of the lists is empty
145                // This can only be represented by the empty action
146                // This implies an action group
147                (None, _) | (_, None) => {
148                    write!(f, "")?;
149                }
150                // Both list are non empty
151                (Some(ps), Some(rs)) => {
152                    write!(f, " appliesTo {{")?;
153                    write!(f, "\n  principal: ")?;
154                    fmt_vec(f, ps)?;
155                    write!(f, ",\n  resource: ")?;
156                    fmt_vec(f, rs)?;
157                    write!(f, ",\n  context: {}", &spec.context.0)?;
158                    write!(f, "\n}}")?;
159                }
160            }
161        }
162        // No `appliesTo` key: action does not apply to anything
163        Ok(())
164    }
165}
166
167/// Error converting a schema to the Cedar syntax
168#[derive(Debug, Diagnostic, Error)]
169pub enum ToCedarSchemaSyntaxError {
170    /// Collisions between names prevented the conversion to the Cedar syntax
171    #[diagnostic(transparent)]
172    #[error(transparent)]
173    NameCollisions(#[from] NameCollisionsError),
174}
175
176/// Duplicate names were found in the schema
177#[derive(Debug, Error, Diagnostic)]
178#[error("There are name collisions: [{}]", .names.iter().join(", "))]
179pub struct NameCollisionsError {
180    /// Names that had collisions
181    names: NonEmpty<SmolStr>,
182}
183
184impl NameCollisionsError {
185    /// Get the names that had collisions
186    pub fn names(&self) -> impl Iterator<Item = &str> {
187        self.names.iter().map(smol_str::SmolStr::as_str)
188    }
189}
190
191/// Convert a [`json_schema::Fragment`] to a string containing the Cedar schema syntax
192///
193/// As of this writing, this existing code throws an error if any
194/// fully-qualified name in a non-empty namespace is a valid common type and
195/// also a valid entity type.
196//
197// Two notes:
198// 1) This check is more conservative than necessary. Schemas are allowed to
199// shadow an entity type with a common type declaration in the same namespace;
200// see RFCs 24 and 70. What the Cedar syntax can't express is if, in that
201// situation, we then specifically refer to the shadowed entity type name.  But
202// it's harder to walk all type references than it is to walk all type
203// declarations, so the conservative code here is fine; we can always make it
204// less conservative in the future without breaking people.
205// 2) This code is also likely the cause of #1063; see that issue
206pub fn json_schema_to_cedar_schema_str<N: Display>(
207    json_schema: &json_schema::Fragment<N>,
208) -> Result<String, ToCedarSchemaSyntaxError> {
209    let mut name_collisions: Vec<SmolStr> = Vec::new();
210    for (name, ns) in json_schema.0.iter().filter(|(name, _)| !name.is_none()) {
211        let entity_types: HashSet<SmolStr> = ns
212            .entity_types
213            .keys()
214            .map(|ty_name| {
215                RawName::new_from_unreserved(ty_name.clone())
216                    .qualify_with_name(name.as_ref())
217                    .to_smolstr()
218            })
219            .collect();
220        let common_types: HashSet<SmolStr> = ns
221            .common_types
222            .keys()
223            .map(|ty_name| {
224                RawName::new_from_unreserved(ty_name.clone().into())
225                    .qualify_with_name(name.as_ref())
226                    .to_smolstr()
227            })
228            .collect();
229        name_collisions.extend(entity_types.intersection(&common_types).cloned());
230    }
231    if let Some(non_empty_collisions) = NonEmpty::from_vec(name_collisions) {
232        return Err(NameCollisionsError {
233            names: non_empty_collisions,
234        }
235        .into());
236    }
237    Ok(json_schema.to_string())
238}
239
240#[cfg(test)]
241mod tests {
242    use cedar_policy_core::extensions::Extensions;
243
244    use crate::cedar_schema::parser::parse_cedar_schema_fragment;
245
246    #[test]
247    fn rfc_example() {
248        let src = "entity User = {
249            jobLevel: Long,
250          } tags Set<String>;
251          entity Document = {
252            owner: User,
253          } tags Set<String>;";
254        let (cedar_schema, _) =
255            parse_cedar_schema_fragment(src, Extensions::none()).expect("should parse");
256        let printed_cedar_schema = cedar_schema.to_cedarschema().expect("should convert");
257        let (parsed_cedar_schema, _) =
258            parse_cedar_schema_fragment(&printed_cedar_schema, Extensions::none())
259                .expect("should parse");
260        assert_eq!(cedar_schema, parsed_cedar_schema);
261    }
262}