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 thiserror::Error;
26
27use crate::{json_schema, RawName};
28use cedar_policy_core::{ast::InternalName, impl_diagnostic_from_method_on_nonempty_field};
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{}}}\n", def.annotations, def)?,
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.annotations, ty.ty)?
46        }
47        for (n, ty) in &self.entity_types {
48            writeln!(f, "{}entity {n}{};", ty.annotations, ty)?
49        }
50        for (n, a) in &self.actions {
51            writeln!(f, "{}action \"{}\"{};", a.annotations, n.escape_debug(), a)?
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                ty.annotations,
85                n.escape_debug(),
86                if ty.required { "" } else { "?" },
87                ty.ty
88            )?;
89            if i < (self.attributes.len() - 1) {
90                writeln!(f, ", ")?;
91            }
92        }
93        write!(f, "}}")?;
94        Ok(())
95    }
96}
97
98fn fmt_non_empty_slice<T: Display>(
99    f: &mut std::fmt::Formatter<'_>,
100    (head, tail): (&T, &[T]),
101) -> std::fmt::Result {
102    write!(f, "[{head}")?;
103    for e in tail {
104        write!(f, ", {e}")?;
105    }
106    write!(f, "]")
107}
108
109impl<N: Display> Display for json_schema::EntityType<N> {
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        if let Some(non_empty) = self.member_of_types.split_first() {
112            write!(f, " in ")?;
113            fmt_non_empty_slice(f, non_empty)?;
114        }
115
116        let ty = &self.shape;
117        // Don't print `= { }`
118        if !ty.is_empty_record() {
119            write!(f, " = {ty}")?;
120        }
121
122        if let Some(tags) = &self.tags {
123            write!(f, " tags {tags}")?;
124        }
125
126        Ok(())
127    }
128}
129
130impl<N: Display> Display for json_schema::ActionType<N> {
131    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132        if let Some(parents) = self.member_of.as_ref().and_then(|refs| refs.split_first()) {
133            write!(f, " in ")?;
134            fmt_non_empty_slice(f, parents)?;
135        }
136        if let Some(spec) = &self.applies_to {
137            match (
138                spec.principal_types.split_first(),
139                spec.resource_types.split_first(),
140            ) {
141                // One of the lists is empty
142                // This can only be represented by the empty action
143                // This implies an action group
144                (None, _) | (_, None) => {
145                    write!(f, "")?;
146                }
147                // Both list are non empty
148                (Some(ps), Some(rs)) => {
149                    write!(f, " appliesTo {{")?;
150                    write!(f, "\n  principal: ")?;
151                    fmt_non_empty_slice(f, ps)?;
152                    write!(f, ",\n  resource: ")?;
153                    fmt_non_empty_slice(f, rs)?;
154                    write!(f, ",\n  context: {}", &spec.context.0)?;
155                    write!(f, "\n}}")?;
156                }
157            }
158        }
159        // No `appliesTo` key: action does not apply to anything
160        Ok(())
161    }
162}
163
164/// Error converting a schema to the Cedar syntax
165#[derive(Debug, Diagnostic, Error)]
166pub enum ToCedarSchemaSyntaxError {
167    /// Collisions between names prevented the conversion to the Cedar syntax
168    #[diagnostic(transparent)]
169    #[error(transparent)]
170    NameCollisions(#[from] NameCollisionsError),
171}
172
173/// Duplicate names were found in the schema
174//
175// This is NOT a publicly exported error type.
176#[derive(Debug, Error)]
177#[error("There are name collisions: [{}]", .names.iter().join(", "))]
178pub struct NameCollisionsError {
179    /// Names that had collisions
180    names: NonEmpty<InternalName>,
181}
182
183impl Diagnostic for NameCollisionsError {
184    impl_diagnostic_from_method_on_nonempty_field!(names, loc);
185}
186
187impl NameCollisionsError {
188    /// Get the names that had collisions
189    pub fn names(&self) -> impl Iterator<Item = &InternalName> {
190        self.names.iter()
191    }
192}
193
194/// Convert a [`json_schema::Fragment`] to a string containing the Cedar schema syntax
195///
196/// As of this writing, this existing code throws an error if any
197/// fully-qualified name in a non-empty namespace is a valid common type and
198/// also a valid entity type.
199//
200// Two notes:
201// 1) This check is more conservative than necessary. Schemas are allowed to
202// shadow an entity type with a common type declaration in the same namespace;
203// see RFCs 24 and 70. What the Cedar syntax can't express is if, in that
204// situation, we then specifically refer to the shadowed entity type name.  But
205// it's harder to walk all type references than it is to walk all type
206// declarations, so the conservative code here is fine; we can always make it
207// less conservative in the future without breaking people.
208// 2) This code is also likely the cause of #1063; see that issue
209pub fn json_schema_to_cedar_schema_str<N: Display>(
210    json_schema: &json_schema::Fragment<N>,
211) -> Result<String, ToCedarSchemaSyntaxError> {
212    let mut name_collisions: Vec<InternalName> = Vec::new();
213    for (name, ns) in json_schema.0.iter().filter(|(name, _)| !name.is_none()) {
214        let entity_types: HashSet<InternalName> = ns
215            .entity_types
216            .keys()
217            .map(|ty_name| {
218                RawName::new_from_unreserved(ty_name.clone()).qualify_with_name(name.as_ref())
219            })
220            .collect();
221        let common_types: HashSet<InternalName> = ns
222            .common_types
223            .keys()
224            .map(|ty_name| {
225                RawName::new_from_unreserved(ty_name.clone().into())
226                    .qualify_with_name(name.as_ref())
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    #[track_caller]
247    fn test_round_trip(src: &str) {
248        let (cedar_schema, _) =
249            parse_cedar_schema_fragment(src, Extensions::none()).expect("should parse");
250        let printed_cedar_schema = cedar_schema.to_cedarschema().expect("should convert");
251        let (parsed_cedar_schema, _) =
252            parse_cedar_schema_fragment(&printed_cedar_schema, Extensions::none())
253                .expect("should parse");
254        assert_eq!(cedar_schema, parsed_cedar_schema);
255    }
256
257    #[test]
258    fn rfc_example() {
259        let src = "entity User = {
260            jobLevel: Long,
261          } tags Set<String>;
262          entity Document = {
263            owner: User,
264          } tags Set<String>;";
265        test_round_trip(src);
266    }
267
268    #[test]
269    fn annotations() {
270        let src = r#"@doc("this is the namespace")
271namespace TinyTodo {
272    @doc("a common type representing a task")
273    type Task = {
274        @doc("task id")
275        "id": Long,
276        "name": String,
277        "state": String,
278    };
279    @doc("a common type representing a set of tasks")
280    type Tasks = Set<Task>;
281
282    @doc("an entity type representing a list")
283    @docComment("any entity type is a child of type `Application`")
284    entity List in [Application] = {
285        @doc("editors of a list")
286        "editors": Team,
287        "name": String,
288        "owner": User,
289        @doc("readers of a list")
290        "readers": Team,
291        "tasks": Tasks,
292    };
293
294    @doc("actions that a user can operate on a list")
295    action DeleteList, GetList, UpdateList appliesTo {
296        principal: [User],
297        resource: [List]
298    };
299}"#;
300        test_round_trip(src);
301    }
302}