cedar_policy/ffi/
utils.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//! Utility functions and types for JSON interface
18use crate::{PolicyId, SchemaWarning, SlotId};
19use miette::miette;
20use miette::WrapErr;
21use serde::{Deserialize, Serialize};
22use std::collections::BTreeSet;
23use std::{collections::HashMap, str::FromStr};
24
25// Publicly expose the `JsonValueWithNoDuplicateKeys` type so that the
26// `*_json_str` APIs will correctly error if the input JSON string contains
27// duplicate keys.
28pub use cedar_policy_core::jsonvalue::JsonValueWithNoDuplicateKeys;
29
30#[cfg(feature = "wasm")]
31extern crate tsify;
32
33/// Structure of the JSON output representing one `miette` error
34#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize, Serialize, Default)]
35#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
36#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
37#[serde(rename_all = "camelCase")]
38#[serde(deny_unknown_fields)]
39pub struct DetailedError {
40    /// Main error message, including both the `miette` "message" and the
41    /// `miette` "causes" (uses `miette`'s default `Display` output)
42    pub message: String,
43    /// Help message, providing additional information about the error or help resolving it
44    pub help: Option<String>,
45    /// Error code
46    pub code: Option<String>,
47    /// URL for more information about the error
48    pub url: Option<String>,
49    /// Severity
50    pub severity: Option<Severity>,
51    /// Source labels (ranges)
52    #[serde(default)]
53    pub source_locations: Vec<SourceLabel>,
54    /// Related errors
55    #[serde(default)]
56    pub related: Vec<DetailedError>,
57}
58
59impl FromStr for DetailedError {
60    type Err = std::convert::Infallible;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        Ok(DetailedError {
64            message: s.to_string(),
65            help: None,
66            code: None,
67            url: None,
68            severity: None,
69            source_locations: Vec::new(),
70            related: Vec::new(),
71        })
72    }
73}
74
75/// Exactly like `miette::Severity` but implements `Hash`
76///
77/// If `miette::Severity` adds `derive(Hash)` in the future, we can remove this
78#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Deserialize, Serialize)]
79#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
80#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
81#[serde(rename_all = "camelCase")]
82pub enum Severity {
83    /// Advice (the lowest severity)
84    Advice,
85    /// Warning
86    Warning,
87    /// Error (the highest severity)
88    Error,
89}
90
91impl From<miette::Severity> for Severity {
92    fn from(severity: miette::Severity) -> Self {
93        match severity {
94            miette::Severity::Advice => Self::Advice,
95            miette::Severity::Warning => Self::Warning,
96            miette::Severity::Error => Self::Error,
97        }
98    }
99}
100
101/// Structure of the JSON output representing a `miette` source label (range)
102#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize, Serialize)]
103#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
104#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
105#[serde(rename_all = "camelCase")]
106#[serde(deny_unknown_fields)]
107pub struct SourceLabel {
108    /// Text of the label (if any)
109    pub label: Option<String>,
110    /// Source location (range) of the label
111    #[serde(flatten)]
112    pub loc: SourceLocation,
113}
114
115/// A range of source code representing the location of an error or warning.
116#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Deserialize, Serialize)]
117#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
118#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
119#[serde(rename_all = "camelCase")]
120#[serde(deny_unknown_fields)]
121pub struct SourceLocation {
122    /// Start of the source location (in bytes)
123    pub start: usize,
124    /// End of the source location (in bytes)
125    pub end: usize,
126}
127
128impl From<miette::LabeledSpan> for SourceLabel {
129    fn from(span: miette::LabeledSpan) -> Self {
130        Self {
131            label: span.label().map(ToString::to_string),
132            loc: SourceLocation {
133                start: span.offset(),
134                end: span.offset() + span.len(),
135            },
136        }
137    }
138}
139
140impl<'a, E: miette::Diagnostic + ?Sized> From<&'a E> for DetailedError {
141    fn from(diag: &'a E) -> Self {
142        Self {
143            message: {
144                let mut s = diag.to_string();
145                let mut source = diag.source();
146                while let Some(e) = source {
147                    s.push_str(": ");
148                    s.push_str(&e.to_string());
149                    source = e.source();
150                }
151                s
152            },
153            help: diag.help().map(|h| h.to_string()),
154            code: diag.code().map(|c| c.to_string()),
155            url: diag.url().map(|u| u.to_string()),
156            severity: diag.severity().map(Into::into),
157            source_locations: diag
158                .labels()
159                .map(|labels| labels.map(Into::into).collect())
160                .unwrap_or_default(),
161            related: diag
162                .related()
163                .map(|errs| errs.map(std::convert::Into::into).collect())
164                .unwrap_or_default(),
165        }
166    }
167}
168
169impl From<miette::Report> for DetailedError {
170    fn from(report: miette::Report) -> Self {
171        let diag: &dyn miette::Diagnostic = report.as_ref();
172        diag.into()
173    }
174}
175
176/// Wrapper around a JSON value describing an entity uid in either explicit or
177/// implicit `__entity` form. Expects the same format as [`crate::EntityUid::from_json`].
178#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
179#[repr(transparent)]
180#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
181#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
182pub struct EntityUid(
183    #[cfg_attr(feature = "wasm", tsify(type = "EntityUidJson"))] JsonValueWithNoDuplicateKeys,
184);
185
186impl EntityUid {
187    /// Parses the given [`EntityUid`] into a [`crate::EntityUid`].
188    /// `category` is an optional note on the type of entity uid being parsed
189    /// for better error messages.
190    ///
191    /// # Errors
192    ///
193    /// Will return `Err` if the input JSON cannot be deserialized as a
194    /// [`crate::EntityUid`].
195    pub fn parse(self, category: Option<&str>) -> Result<crate::EntityUid, miette::Report> {
196        crate::EntityUid::from_json(self.0.into())
197            .wrap_err_with(|| format!("failed to parse {}", category.unwrap_or("entity uid")))
198    }
199}
200
201#[doc(hidden)]
202impl From<serde_json::Value> for EntityUid {
203    fn from(json: serde_json::Value) -> Self {
204        Self(json.into())
205    }
206}
207
208/// Wrapper around a JSON value describing a context. Expects the same format
209/// as [`crate::Context::from_json_value`].
210/// See <https://docs.cedarpolicy.com/auth/entities-syntax.html>
211#[derive(Debug, Serialize, Deserialize)]
212#[repr(transparent)]
213#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
214#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
215pub struct Context(
216    #[cfg_attr(feature = "wasm", tsify(type = "Record<string, CedarValueJson>"))]
217    JsonValueWithNoDuplicateKeys,
218);
219
220impl Context {
221    /// Parses the given [`Context`] into a [`crate::Context`]
222    ///
223    /// # Errors
224    ///
225    /// Will return `Err` if the input JSON cannot be deserialized as a
226    /// [`crate::Context`].
227    pub fn parse(
228        self,
229        schema_ref: Option<&crate::Schema>,
230        action_ref: Option<&crate::EntityUid>,
231    ) -> Result<crate::Context, miette::Report> {
232        crate::Context::from_json_value(
233            self.0.into(),
234            match (schema_ref, action_ref) {
235                (Some(s), Some(a)) => Some((s, a)),
236                _ => None,
237            },
238        )
239        .map_err(Into::into)
240    }
241}
242
243#[doc(hidden)]
244impl From<serde_json::Value> for Context {
245    fn from(json: serde_json::Value) -> Self {
246        Self(json.into())
247    }
248}
249
250/// Wrapper around a JSON value describing a set of entities. Expects the same
251/// format as [`crate::Entities::from_json_value`].
252/// See <https://docs.cedarpolicy.com/auth/entities-syntax.html>
253#[derive(Debug, Serialize, Deserialize)]
254#[repr(transparent)]
255#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
256#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
257pub struct Entities(
258    #[cfg_attr(feature = "wasm", tsify(type = "Array<EntityJson>"))] JsonValueWithNoDuplicateKeys,
259);
260
261impl Entities {
262    /// Parses the given [`Entities`] into a [`crate::Entities`]
263    ///
264    /// # Errors
265    ///
266    /// Will return `Err` if the input JSON cannot be deserialized as a
267    /// [`crate::Entities`].
268    pub fn parse(
269        self,
270        opt_schema: Option<&crate::Schema>,
271    ) -> Result<crate::Entities, miette::Report> {
272        crate::Entities::from_json_value(self.0.into(), opt_schema).map_err(Into::into)
273    }
274}
275
276#[doc(hidden)]
277impl From<serde_json::Value> for Entities {
278    fn from(json: serde_json::Value) -> Self {
279        Self(json.into())
280    }
281}
282
283/// Represents a static policy in either the Cedar or JSON policy format
284#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
285#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
286#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
287#[serde(untagged)]
288#[serde(
289    expecting = "expected a static policy in the Cedar or JSON policy format (with no duplicate keys)"
290)]
291pub enum Policy {
292    /// Policy in the Cedar policy format. See <https://docs.cedarpolicy.com/policies/syntax-policy.html>
293    Cedar(String),
294    /// Policy in Cedar's JSON policy format. See <https://docs.cedarpolicy.com/policies/json-format.html>
295    Json(#[cfg_attr(feature = "wasm", tsify(type = "PolicyJson"))] JsonValueWithNoDuplicateKeys),
296}
297
298impl Policy {
299    /// Parse a [`Policy`] into a [`crate::Policy`]. Takes an optional id
300    /// argument that sets the policy id. If the argument is `None` then a
301    /// default id will be assigned. Will return an error if passed a template.
302    pub(super) fn parse(self, id: Option<PolicyId>) -> Result<crate::Policy, miette::Report> {
303        let msg = id
304            .clone()
305            .map_or(String::new(), |id| format!(" with id `{id}`"));
306        match self {
307            Self::Cedar(str) => crate::Policy::parse(id, str)
308                .wrap_err(format!("failed to parse policy{msg} from string")),
309            Self::Json(json) => crate::Policy::from_json(id, json.into())
310                .wrap_err(format!("failed to parse policy{msg} from JSON")),
311        }
312    }
313
314    /// Get valid principals, actions, and resources.
315    ///
316    /// # Errors
317    ///
318    /// Returns an error result if `self` cannot be parsed as a
319    /// [`crate::Policy`] or if `s` cannot be parsed as a [`crate::Schema`].
320    pub fn get_valid_request_envs(
321        self,
322        s: Schema,
323    ) -> Result<
324        (
325            impl Iterator<Item = String>,
326            impl Iterator<Item = String>,
327            impl Iterator<Item = String>,
328        ),
329        miette::Report,
330    > {
331        let t = self.parse(None)?;
332        let (s, _) = s.parse()?;
333        let mut principals = BTreeSet::new();
334        let mut actions = BTreeSet::new();
335        let mut resources = BTreeSet::new();
336        for env in t.get_valid_request_envs(&s) {
337            principals.insert(env.principal.to_string());
338            actions.insert(env.action.to_string());
339            resources.insert(env.resource.to_string());
340        }
341        Ok((
342            principals.into_iter(),
343            actions.into_iter(),
344            resources.into_iter(),
345        ))
346    }
347}
348
349/// Represents a policy template in either the Cedar or JSON policy format.
350#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
351#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
352#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
353#[serde(untagged)]
354#[serde(
355    expecting = "expected a policy template in the Cedar or JSON policy format (with no duplicate keys)"
356)]
357pub enum Template {
358    /// Template in the Cedar policy format. See <https://docs.cedarpolicy.com/policies/syntax-policy.html>
359    Cedar(String),
360    /// Template in Cedar's JSON policy format. See <https://docs.cedarpolicy.com/policies/json-format.html>
361    Json(#[cfg_attr(feature = "wasm", tsify(type = "PolicyJson"))] JsonValueWithNoDuplicateKeys),
362}
363
364impl Template {
365    /// Parse a [`Template`] into a [`crate::Template`]. Takes an optional id
366    /// argument that sets the template id. If the argument is `None` then a
367    /// default id will be assigned.
368    pub(super) fn parse(self, id: Option<PolicyId>) -> Result<crate::Template, miette::Report> {
369        let msg = id
370            .clone()
371            .map(|id| format!(" with id `{id}`"))
372            .unwrap_or_default();
373        match self {
374            Self::Cedar(str) => crate::Template::parse(id, str)
375                .wrap_err(format!("failed to parse template{msg} from string")),
376            Self::Json(json) => crate::Template::from_json(id, json.into())
377                .wrap_err(format!("failed to parse template{msg} from JSON")),
378        }
379    }
380
381    /// Parse a [`Template`] into a [`crate::Template`] and add it into the
382    /// provided [`crate::PolicySet`].
383    pub(super) fn parse_and_add_to_set(
384        self,
385        id: Option<PolicyId>,
386        policies: &mut crate::PolicySet,
387    ) -> Result<(), miette::Report> {
388        let msg = id
389            .clone()
390            .map(|id| format!(" with id `{id}`"))
391            .unwrap_or_default();
392        let template = self.parse(id)?;
393        policies
394            .add_template(template)
395            .wrap_err(format!("failed to add template{msg} to policy set"))
396    }
397
398    /// Get valid principals, actions, and resources.
399    ///
400    /// # Errors
401    ///
402    /// Returns an error result if `self` cannot be parsed as a
403    /// [`crate::Template`] or if `s` cannot be parsed as a [`crate::Schema`].
404    pub fn get_valid_request_envs(
405        self,
406        s: Schema,
407    ) -> Result<
408        (
409            impl Iterator<Item = String>,
410            impl Iterator<Item = String>,
411            impl Iterator<Item = String>,
412        ),
413        miette::Report,
414    > {
415        let t = self.parse(None)?;
416        let (s, _) = s.parse()?;
417        let mut principals = BTreeSet::new();
418        let mut actions = BTreeSet::new();
419        let mut resources = BTreeSet::new();
420        for env in t.get_valid_request_envs(&s) {
421            principals.insert(env.principal.to_string());
422            actions.insert(env.action.to_string());
423            resources.insert(env.resource.to_string());
424        }
425        Ok((
426            principals.into_iter(),
427            actions.into_iter(),
428            resources.into_iter(),
429        ))
430    }
431}
432
433/// Represents a set of static policies
434#[derive(Debug, Serialize, Deserialize)]
435#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
436#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
437#[serde(untagged)]
438#[serde(
439    expecting = "expected a static policy set represented by a string, JSON array, or JSON object (with no duplicate keys)"
440)]
441pub enum StaticPolicySet {
442    /// Multiple policies as a concatenated string. Requires policies in the
443    /// Cedar (non-JSON) format.
444    Concatenated(String),
445    /// Multiple policies as a set
446    Set(Vec<Policy>),
447    /// Multiple policies as a hashmap where the policy id is the key
448    #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
449    Map(HashMap<PolicyId, Policy>),
450}
451
452impl StaticPolicySet {
453    /// Parse a [`StaticPolicySet`] into a [`crate::PolicySet`]
454    pub(super) fn parse(self) -> Result<crate::PolicySet, Vec<miette::Report>> {
455        match self {
456            Self::Concatenated(str) => {
457                let policies = crate::PolicySet::from_str(&str)
458                    .wrap_err("failed to parse policies from string")
459                    .map_err(|e| vec![e])?;
460                // make sure the parsed policies are all static policies
461                if policies.templates().count() > 0 {
462                    Err(vec![miette!("static policy set includes a template")])
463                } else {
464                    Ok(policies)
465                }
466            }
467            Self::Set(set) => {
468                let mut errs = Vec::new();
469                let policies = set
470                    .into_iter()
471                    .map(|policy| policy.parse(None))
472                    .filter_map(|r| r.map_err(|e| errs.push(e)).ok())
473                    .collect::<Vec<_>>();
474                if errs.is_empty() {
475                    crate::PolicySet::from_policies(policies).map_err(|e| vec![e.into()])
476                } else {
477                    Err(errs)
478                }
479            }
480            Self::Map(map) => {
481                let mut errs = Vec::new();
482                let policies = map
483                    .into_iter()
484                    .map(|(id, policy)| policy.parse(Some(id)))
485                    .filter_map(|r| r.map_err(|e| errs.push(e)).ok())
486                    .collect::<Vec<_>>();
487                if errs.is_empty() {
488                    crate::PolicySet::from_policies(policies).map_err(|e| vec![e.into()])
489                } else {
490                    Err(errs)
491                }
492            }
493        }
494    }
495}
496
497impl Default for StaticPolicySet {
498    fn default() -> Self {
499        Self::Set(Vec::new())
500    }
501}
502
503/// Represents a template-linked policy
504#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
505#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
506#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
507#[serde(rename_all = "camelCase")]
508#[serde(deny_unknown_fields)]
509pub struct TemplateLink {
510    /// Id of the template to link against
511    template_id: PolicyId,
512    /// Id of the generated policy
513    new_id: PolicyId,
514    /// Values for the slots; keys must be slot ids (i.e., `?principal` or `?resource`)
515    #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
516    values: HashMap<SlotId, EntityUid>,
517}
518
519impl TemplateLink {
520    /// Parse a [`TemplateLink`] and add the linked policy into the provided [`crate::PolicySet`]
521    pub(super) fn parse_and_add_to_set(
522        self,
523        policies: &mut crate::PolicySet,
524    ) -> Result<(), miette::Report> {
525        let values: HashMap<_, _> = self
526            .values
527            .into_iter()
528            .map(|(slot, euid)| euid.parse(None).map(|euid| (slot, euid)))
529            .collect::<Result<HashMap<_, _>, _>>()
530            .wrap_err("failed to parse link values")?;
531        policies
532            .link(self.template_id, self.new_id, values)
533            .map_err(miette::Report::new)
534    }
535}
536
537/// Represents a policy set, including static policies, templates, and template links
538#[derive(Debug, Serialize, Deserialize)]
539#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
540#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
541#[serde(rename_all = "camelCase")]
542#[serde(deny_unknown_fields)]
543pub struct PolicySet {
544    /// static policies
545    #[serde(default)]
546    static_policies: StaticPolicySet,
547    /// a map from template id to template content
548    #[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
549    #[serde(default)]
550    templates: HashMap<PolicyId, Template>,
551    /// template links
552    #[serde(default)]
553    template_links: Vec<TemplateLink>,
554}
555
556impl PolicySet {
557    /// Parse a [`PolicySet`] into a [`crate::PolicySet`]
558    ///
559    /// # Errors
560    ///
561    /// Will return errors if any of the policies, templates or template links
562    /// in [`PolicySet`] cannot be parsed or added to the [`crate::PolicySet`].
563    pub fn parse(self) -> Result<crate::PolicySet, Vec<miette::Report>> {
564        let mut errs = Vec::new();
565        // Parse static policies
566        let mut policies = self.static_policies.parse().unwrap_or_else(|mut e| {
567            errs.append(&mut e);
568            crate::PolicySet::new()
569        });
570        // Parse templates & add them to the policy set
571        self.templates.into_iter().for_each(|(id, template)| {
572            template
573                .parse_and_add_to_set(Some(id), &mut policies)
574                .unwrap_or_else(|e| errs.push(e));
575        });
576        // Parse template links & add the resulting policies to the policy set
577        self.template_links.into_iter().for_each(|link| {
578            link.parse_and_add_to_set(&mut policies)
579                .unwrap_or_else(|e| errs.push(e));
580        });
581        // Return an error or the final policy set
582        if !errs.is_empty() {
583            return Err(errs);
584        }
585        Ok(policies)
586    }
587}
588
589#[cfg(test)]
590impl PolicySet {
591    /// Create an empty [`PolicySet`]
592    pub(super) fn new() -> Self {
593        Self {
594            static_policies: StaticPolicySet::Set(Vec::new()),
595            templates: HashMap::new(),
596            template_links: Vec::new(),
597        }
598    }
599}
600
601/// Represents a schema in either the Cedar or JSON schema format
602#[derive(Debug, Serialize, Deserialize)]
603#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
604#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
605#[serde(untagged)]
606#[serde(
607    expecting = "expected a schema in the Cedar or JSON policy format (with no duplicate keys)"
608)]
609pub enum Schema {
610    /// Schema in the Cedar schema format. See <https://docs.cedarpolicy.com/schema/human-readable-schema.html>
611    Cedar(String),
612    /// Schema in Cedar's JSON schema format. See <https://docs.cedarpolicy.com/schema/json-schema.html>
613    Json(
614        #[cfg_attr(feature = "wasm", tsify(type = "SchemaJson<string>"))]
615        JsonValueWithNoDuplicateKeys,
616    ),
617}
618
619impl Schema {
620    /// Parse a [`Schema`] into a [`crate::Schema`]
621    pub(super) fn parse(
622        self,
623    ) -> Result<(crate::Schema, Box<dyn Iterator<Item = SchemaWarning>>), miette::Report> {
624        let (schema_frag, warnings) = self.parse_schema_fragment()?;
625        Ok((schema_frag.try_into()?, warnings))
626    }
627
628    /// Return a [`crate::SchemaFragment`], which can be printed with `.to_string()`
629    /// and converted to JSON with `.to_json()`.
630    pub(super) fn parse_schema_fragment(
631        self,
632    ) -> Result<
633        (
634            crate::SchemaFragment,
635            Box<dyn Iterator<Item = SchemaWarning>>,
636        ),
637        miette::Report,
638    > {
639        match self {
640            Self::Cedar(str) => crate::SchemaFragment::from_cedarschema_str(&str)
641                .map(|(sch, warnings)| {
642                    (
643                        sch,
644                        Box::new(warnings) as Box<dyn Iterator<Item = SchemaWarning>>,
645                    )
646                })
647                .wrap_err("failed to parse schema from string"),
648            Self::Json(val) => crate::SchemaFragment::from_json_value(val.into())
649                .map(|sch| {
650                    (
651                        sch,
652                        Box::new(std::iter::empty()) as Box<dyn Iterator<Item = SchemaWarning>>,
653                    )
654                })
655                .wrap_err("failed to parse schema from JSON"),
656        }
657    }
658}
659
660pub(super) struct WithWarnings<T> {
661    pub t: T,
662    pub warnings: Vec<miette::Report>,
663}
664
665/// Testing utilities used here and elsewhere
666// PANIC SAFETY unit tests
667#[allow(clippy::panic, clippy::indexing_slicing)]
668// Also disable some other clippy lints that are unimportant for testing code
669#[allow(clippy::module_name_repetitions, clippy::missing_panics_doc)]
670#[cfg(test)]
671pub mod test_utils {
672    use super::*;
673
674    /// Assert that an error has the specified message and help fields.
675    #[track_caller]
676    pub fn assert_error_matches(err: &DetailedError, msg: &str, help: Option<&str>) {
677        assert_eq!(err.message, msg, "did not see the expected error message");
678        assert_eq!(
679            err.help,
680            help.map(Into::into),
681            "did not see the expected help message"
682        );
683    }
684
685    /// Assert that a vector (of errors) has the expected length
686    #[track_caller]
687    pub fn assert_length_matches<T: std::fmt::Debug>(errs: &[T], n: usize) {
688        assert_eq!(
689            errs.len(),
690            n,
691            "expected {n} error(s) but saw {}",
692            errs.len()
693        );
694    }
695
696    /// Assert that a vector contains exactly one error with the specified
697    /// message and help text.
698    #[track_caller]
699    pub fn assert_exactly_one_error(errs: &[DetailedError], msg: &str, help: Option<&str>) {
700        assert_length_matches(errs, 1);
701        assert_error_matches(&errs[0], msg, help);
702    }
703}
704
705// PANIC SAFETY unit tests
706#[allow(clippy::panic, clippy::indexing_slicing)]
707// Also disable some other clippy lints that are unimportant for testing code
708#[allow(clippy::too_many_lines)]
709#[cfg(test)]
710mod test {
711    use super::*;
712    use cedar_policy_core::test_utils::*;
713    use serde_json::json;
714    use test_utils::assert_length_matches;
715
716    #[test]
717    fn test_policy_parser() {
718        // A string literal will be parsed as a policy in the Cedar syntax
719        let policy_json = json!("permit(principal == User::\"alice\", action, resource);");
720        let policy: Policy =
721            serde_json::from_value(policy_json).expect("failed to parse from JSON");
722        policy.parse(None).expect("failed to convert to policy");
723
724        // A JSON object will be parsed as a policy in the JSON syntax
725        let policy_json = json!({
726            "effect": "permit",
727            "principal": {
728                "op": "==",
729                "entity": { "type": "User", "id": "alice" }
730            },
731            "action": {
732                "op": "All"
733            },
734            "resource": {
735                "op": "All"
736            },
737            "conditions": []
738        });
739        let policy: Policy =
740            serde_json::from_value(policy_json).expect("failed to parse from JSON");
741        policy.parse(None).expect("failed to convert to policy");
742
743        // Invalid Cedar syntax
744        let src = "foo(principal == User::\"alice\", action, resource);";
745        let policy: Policy = serde_json::from_value(json!(src)).expect("failed to parse from JSON");
746        let err = policy
747            .parse(None)
748            .expect_err("should have failed to convert to policy");
749        expect_err(
750            src,
751            &err,
752            &ExpectedErrorMessageBuilder::error("failed to parse policy from string")
753                .source("invalid policy effect: foo")
754                .exactly_one_underline("foo")
755                .help("effect must be either `permit` or `forbid`")
756                .build(),
757        );
758
759        // Not a static policy
760        let src = "permit(principal == ?principal, action, resource);";
761        let policy: Policy =
762            serde_json::from_value(json!(src)).expect("failed to parse from string");
763        let err = policy
764            .parse(None)
765            .expect_err("should have failed to convert to policy");
766        expect_err(
767            src,
768            &err,
769            &ExpectedErrorMessageBuilder::error("failed to parse policy from string")
770                .source("expected a static policy, got a template containing the slot ?principal")
771                .exactly_one_underline("?principal")
772                .help("try removing the template slot(s) from this policy")
773                .build(),
774        );
775
776        // Not a single policy
777        let src = "permit(principal == User::\"alice\", action, resource); permit(principal == User::\"bob\", action, resource);";
778        let policy: Policy =
779            serde_json::from_value(json!(src)).expect("failed to parse from string");
780        let err = policy
781            .parse(None)
782            .expect_err("should have failed to convert to policy");
783        expect_err(
784            src,
785            &err,
786            &ExpectedErrorMessageBuilder::error("failed to parse policy from string")
787                .source("unexpected token `permit`")
788                .exactly_one_underline("permit")
789                .build(),
790        );
791
792        // Invalid JSON syntax (duplicate keys)
793        // The error message comes from the `serde(expecting = ..)` annotation on `Policy`
794        let policy_json_str = r#"{
795            "effect": "permit",
796            "effect": "forbid"
797        }"#;
798        let err = serde_json::from_str::<Policy>(policy_json_str)
799            .expect_err("should have failed to parse from JSON");
800        assert_eq!(
801            err.to_string(),
802            "expected a static policy in the Cedar or JSON policy format (with no duplicate keys)"
803        );
804    }
805
806    #[test]
807    fn test_template_parser() {
808        // A string literal will be parsed as a template in the Cedar syntax
809        let template_json = json!("permit(principal == ?principal, action, resource);");
810        let template: Template =
811            serde_json::from_value(template_json).expect("failed to parse from JSON");
812        template.parse(None).expect("failed to convert to template");
813
814        // A JSON object will be parsed as a template in the JSON syntax
815        let template_json = json!({
816            "effect": "permit",
817            "principal": {
818                "op": "==",
819                "slot": "?principal"
820            },
821            "action": {
822                "op": "All"
823            },
824            "resource": {
825                "op": "All"
826            },
827            "conditions": []
828        });
829        let template: Template =
830            serde_json::from_value(template_json).expect("failed to parse from JSON");
831        template.parse(None).expect("failed to convert to template");
832
833        // Invalid syntax
834        let src = "permit(principal == ?foo, action, resource);";
835        let template: Template =
836            serde_json::from_value(json!(src)).expect("failed to parse from JSON");
837        let err = template
838            .parse(None)
839            .expect_err("should have failed to convert to template");
840        expect_err(
841            src,
842            &err,
843            &ExpectedErrorMessageBuilder::error("failed to parse template from string")
844                .source("expected an entity uid or matching template slot, found ?foo instead of ?principal")
845                .exactly_one_underline("?foo")
846                .build(),
847        );
848
849        // Static policies cannot be parsed as templates
850        let src = "permit(principal == User::\"alice\", action, resource);";
851        let template: Template =
852            serde_json::from_value(json!(src)).expect("failed to parse from JSON");
853        let err = template
854            .parse(None)
855            .expect_err("should have failed to convert to template");
856        expect_err(
857            src,
858            &err,
859            &ExpectedErrorMessageBuilder::error("failed to parse template from string")
860                .source("expected a template, got a static policy")
861                .help("a template should include slot(s) `?principal` or `?resource`")
862                .exactly_one_underline(src)
863                .build(),
864        );
865    }
866
867    #[test]
868    fn test_static_policy_set_parser() {
869        // A string literal will be parsed as the `Concatenated` variant
870        let policies_json = json!("permit(principal == User::\"alice\", action, resource);");
871        let policies: StaticPolicySet =
872            serde_json::from_value(policies_json).expect("failed to parse from JSON");
873        policies
874            .parse()
875            .expect("failed to convert to static policy set");
876
877        // A JSON array will be parsed as the `Set` variant
878        let policies_json = json!([
879            {
880                "effect": "permit",
881                "principal": {
882                    "op": "==",
883                    "entity": { "type": "User", "id": "alice" }
884                },
885                "action": {
886                    "op": "All"
887                },
888                "resource": {
889                    "op": "All"
890                },
891                "conditions": []
892            },
893            "permit(principal == User::\"bob\", action, resource);"
894        ]);
895        let policies: StaticPolicySet =
896            serde_json::from_value(policies_json).expect("failed to parse from JSON");
897        policies
898            .parse()
899            .expect("failed to convert to static policy set");
900
901        // A JSON object will be parsed as the `Map` variant
902        let policies_json = json!({
903            "policy0": {
904                "effect": "permit",
905                "principal": {
906                    "op": "==",
907                    "entity": { "type": "User", "id": "alice" }
908                },
909                "action": {
910                    "op": "All"
911                },
912                "resource": {
913                    "op": "All"
914                },
915                "conditions": []
916            },
917            "policy1": "permit(principal == User::\"bob\", action, resource);"
918        });
919        let policies: StaticPolicySet =
920            serde_json::from_value(policies_json).expect("failed to parse from JSON");
921        policies
922            .parse()
923            .expect("failed to convert to static policy set");
924
925        // Invalid static policy set - `policy0` is a template
926        let policies_json = json!({
927            "policy0": "permit(principal == ?principal, action, resource);",
928            "policy1": "permit(principal == User::\"bob\", action, resource);"
929        });
930        let policies: StaticPolicySet =
931            serde_json::from_value(policies_json).expect("failed to parse from JSON");
932        let errs = policies
933            .parse()
934            .expect_err("should have failed to convert to static policy set");
935        assert_length_matches(&errs, 1);
936        expect_err(
937            "permit(principal == ?principal, action, resource);",
938            &errs[0],
939            &ExpectedErrorMessageBuilder::error(
940                "failed to parse policy with id `policy0` from string",
941            )
942            .source("expected a static policy, got a template containing the slot ?principal")
943            .exactly_one_underline("?principal")
944            .help("try removing the template slot(s) from this policy")
945            .build(),
946        );
947
948        // Invalid static policy set - the second policy is a template
949        let policies_json = json!(
950            "
951            permit(principal == User::\"alice\", action, resource);
952            permit(principal == ?principal, action, resource);
953        "
954        );
955        let policies: StaticPolicySet =
956            serde_json::from_value(policies_json).expect("failed to parse from JSON");
957        let errs = policies
958            .parse()
959            .expect_err("should have failed to convert to static policy set");
960        assert_length_matches(&errs, 1);
961        expect_err(
962            "permit(principal == ?principal, action, resource);",
963            &errs[0],
964            &ExpectedErrorMessageBuilder::error("static policy set includes a template").build(),
965        );
966
967        // Invalid static policy set - `policy1` is actually multiple policies
968        let policies_json = json!({
969            "policy0": "permit(principal == User::\"alice\", action, resource);",
970            "policy1": "permit(principal == User::\"bob\", action, resource); permit(principal, action, resource);"
971        });
972        let policies: StaticPolicySet =
973            serde_json::from_value(policies_json).expect("failed to parse from JSON");
974        let errs = policies
975            .parse()
976            .expect_err("should have failed to convert to static policy set");
977        assert_length_matches(&errs, 1);
978        expect_err(
979            "permit(principal == User::\"bob\", action, resource); permit(principal, action, resource);",
980            &errs[0],
981            &ExpectedErrorMessageBuilder::error(
982                "failed to parse policy with id `policy1` from string",
983            )
984            .source("unexpected token `permit`")
985            .exactly_one_underline("permit")
986            .build(),
987        );
988
989        // Invalid static policy set - both policies are ill-formed
990        let policies_json = json!({
991            "policy0": "permit(principal, action);",
992            "policy1": "forbid(principal, action);"
993        });
994        let policies: StaticPolicySet =
995            serde_json::from_value(policies_json).expect("failed to parse from JSON");
996        let errs = policies
997            .parse()
998            .expect_err("should have failed to convert to static policy set");
999        assert_length_matches(&errs, 2);
1000        for err in errs {
1001            // hack to account for nondeterministic error ordering
1002            if err
1003                .to_string()
1004                .contains("failed to parse policy with id `policy0`")
1005            {
1006                expect_err(
1007                "permit(principal, action);",
1008                &err,
1009                &ExpectedErrorMessageBuilder::error(
1010                        "failed to parse policy with id `policy0` from string",
1011                    )
1012                    .source("this policy is missing the `resource` variable in the scope")
1013                    .exactly_one_underline("")
1014                    .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
1015                    .build(),
1016            );
1017            } else {
1018                expect_err(
1019                "forbid(principal, action);",
1020                &err,
1021                &ExpectedErrorMessageBuilder::error(
1022                        "failed to parse policy with id `policy1` from string",
1023                    )
1024                    .source("this policy is missing the `resource` variable in the scope")
1025                    .exactly_one_underline("")
1026                    .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
1027                    .build(),
1028            );
1029            }
1030        }
1031    }
1032
1033    #[test]
1034    fn test_policy_set_parser() {
1035        // Empty policy set
1036        let policies_json = json!({});
1037        let policies: PolicySet =
1038            serde_json::from_value(policies_json).expect("failed to parse from JSON");
1039        policies.parse().expect("failed to convert to policy set");
1040
1041        // Example valid policy set
1042        let policies_json = json!({
1043            "staticPolicies": [
1044                {
1045                    "effect": "permit",
1046                    "principal": {
1047                        "op": "==",
1048                        "entity": { "type": "User", "id": "alice" }
1049                    },
1050                    "action": {
1051                        "op": "All"
1052                    },
1053                    "resource": {
1054                        "op": "All"
1055                    },
1056                    "conditions": []
1057                },
1058                "permit(principal == User::\"bob\", action, resource);"
1059            ],
1060            "templates": {
1061                "ID0": "permit(principal == ?principal, action, resource);"
1062            },
1063            "templateLinks": [
1064                {
1065                    "templateId": "ID0",
1066                    "newId": "ID1",
1067                    "values": { "?principal": { "type": "User", "id": "charlie" } }
1068                }
1069            ]
1070        });
1071        let policies: PolicySet =
1072            serde_json::from_value(policies_json).expect("failed to parse from JSON");
1073        policies.parse().expect("failed to convert to policy set");
1074
1075        // Example policy set with a link error - `policy0` is already used
1076        let policies_json = json!({
1077            "staticPolicies": {
1078                "policy0": "permit(principal == User::\"alice\", action, resource);",
1079                "policy1": "permit(principal == User::\"bob\", action, resource);"
1080            },
1081            "templates": {
1082                "template": "permit(principal == ?principal, action, resource);"
1083            },
1084            "templateLinks": [
1085                {
1086                    "templateId": "template",
1087                    "newId": "policy0",
1088                    "values": { "?principal": { "type": "User", "id": "charlie" } }
1089                }
1090            ]
1091        });
1092        let policies: PolicySet =
1093            serde_json::from_value(policies_json).expect("failed to parse from JSON");
1094        let errs = policies
1095            .parse()
1096            .expect_err("should have failed to convert to policy set");
1097        assert_length_matches(&errs, 1);
1098        expect_err(
1099            "",
1100            &errs[0],
1101            &ExpectedErrorMessageBuilder::error("unable to link template")
1102                .source("template-linked policy id `policy0` conflicts with an existing policy id")
1103                .build(),
1104        );
1105    }
1106
1107    #[test]
1108    fn policy_set_parser_is_compatible_with_est_parser() {
1109        // The `PolicySet::parse` function accepts the `est::PolicySet` JSON format
1110        let json = json!({
1111            "staticPolicies": {
1112                "policy1": {
1113                    "effect": "permit",
1114                    "principal": {
1115                        "op": "==",
1116                        "entity": { "type": "User", "id": "alice" }
1117                    },
1118                    "action": {
1119                        "op": "==",
1120                        "entity": { "type": "Action", "id": "view" }
1121                    },
1122                    "resource": {
1123                        "op": "in",
1124                        "entity": { "type": "Folder", "id": "foo" }
1125                    },
1126                    "conditions": []
1127                }
1128            },
1129            "templates": {
1130                "template": {
1131                    "effect" : "permit",
1132                    "principal" : {
1133                        "op" : "==",
1134                        "slot" : "?principal"
1135                    },
1136                    "action" : {
1137                        "op" : "all"
1138                    },
1139                    "resource" : {
1140                        "op" : "all",
1141                    },
1142                    "conditions": []
1143                }
1144            },
1145            "templateLinks" : [
1146                {
1147                    "newId" : "link",
1148                    "templateId" : "template",
1149                    "values" : {
1150                        "?principal" : { "type" : "User", "id" : "bob" }
1151                    }
1152                }
1153            ]
1154        });
1155
1156        // use `crate::PolicySet::from_json_value`
1157        let ast_from_est = crate::PolicySet::from_json_value(json.clone())
1158            .expect("failed to convert to policy set");
1159
1160        // use `PolicySet::parse`
1161        let ffi_policy_set: PolicySet =
1162            serde_json::from_value(json).expect("failed to parse from JSON");
1163        let ast_from_ffi = ffi_policy_set
1164            .parse()
1165            .expect("failed to convert to policy set");
1166
1167        // check that the produced policy sets match
1168        assert_eq!(ast_from_est, ast_from_ffi);
1169    }
1170
1171    #[test]
1172    fn test_schema_parser() {
1173        // A string literal will be parsed as a schema in the Cedar syntax
1174        let schema_json = json!("entity User = {name: String};\nentity Photo;\naction viewPhoto appliesTo {principal: User, resource: Photo};");
1175        let schema: Schema =
1176            serde_json::from_value(schema_json).expect("failed to parse from JSON");
1177        let _ = schema.parse().expect("failed to convert to schema");
1178
1179        // A JSON object will be parsed as a schema in the JSON syntax
1180        let schema_json = json!({
1181            "": {
1182                "entityTypes": {
1183                    "User": {
1184                        "shape": {
1185                            "type": "Record",
1186                            "attributes": {
1187                                "name": {
1188                                    "type": "String"
1189                                }
1190                            }
1191                        }
1192                    },
1193                    "Photo": {}
1194                },
1195                "actions": {
1196                    "viewPhoto": {
1197                        "appliesTo": {
1198                            "principalTypes": [ "User" ],
1199                            "resourceTypes": [ "Photo" ]
1200                        }
1201                    }
1202                }
1203            }
1204        });
1205        let schema: Schema =
1206            serde_json::from_value(schema_json).expect("failed to parse from JSON");
1207        let _ = schema.parse().expect("failed to convert to schema");
1208
1209        // Invalid syntax (the value is a policy)
1210        let src = "permit(principal == User::\"alice\", action, resource);";
1211        let schema: Schema = serde_json::from_value(json!(src)).expect("failed to parse from JSON");
1212        let err = schema
1213            .parse()
1214            .map(|(s, _)| s)
1215            .expect_err("should have failed to convert to schema");
1216        expect_err(
1217            src,
1218            &err,
1219            &ExpectedErrorMessageBuilder::error("failed to parse schema from string")
1220                .exactly_one_underline_with_label(
1221                    "permit",
1222                    "expected `@`, `action`, `entity`, `namespace`, or `type`",
1223                )
1224                .source("error parsing schema: unexpected token `permit`")
1225                .build(),
1226        );
1227    }
1228
1229    #[test]
1230    fn test_detailed_err_from_str() {
1231        let detailed_err = DetailedError::from_str("xxx");
1232        assert_eq!(detailed_err.unwrap().message, "xxx");
1233    }
1234}