Skip to main content

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