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