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 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
568#[cfg(test)]
569impl PolicySet {
570    /// Create an empty [`PolicySet`]
571    pub(super) fn new() -> Self {
572        Self {
573            static_policies: StaticPolicySet::Set(Vec::new()),
574            templates: HashMap::new(),
575            template_links: Vec::new(),
576        }
577    }
578}
579
580/// Represents a schema in either the Cedar or JSON schema format
581#[derive(Debug, Serialize, Deserialize)]
582#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
583#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
584#[serde(untagged)]
585#[serde(
586    expecting = "expected a schema in the Cedar or JSON policy format (with no duplicate keys)"
587)]
588pub enum Schema {
589    /// Schema in the Cedar schema format. See <https://docs.cedarpolicy.com/schema/human-readable-schema.html>
590    Cedar(String),
591    /// Schema in Cedar's JSON schema format. See <https://docs.cedarpolicy.com/schema/json-schema.html>
592    Json(
593        #[cfg_attr(feature = "wasm", tsify(type = "SchemaJson<string>"))]
594        JsonValueWithNoDuplicateKeys,
595    ),
596}
597
598impl Schema {
599    /// Parse a [`Schema`] into a [`crate::Schema`]
600    pub(super) fn parse(
601        self,
602    ) -> Result<(crate::Schema, Box<dyn Iterator<Item = SchemaWarning>>), miette::Report> {
603        let (schema_frag, warnings) = self.parse_schema_fragment()?;
604        Ok((schema_frag.try_into()?, warnings))
605    }
606
607    /// Return a [`crate::SchemaFragment`], which can be printed with `.to_string()`
608    /// and converted to JSON with `.to_json()`.
609    pub(super) fn parse_schema_fragment(
610        self,
611    ) -> Result<
612        (
613            crate::SchemaFragment,
614            Box<dyn Iterator<Item = SchemaWarning>>,
615        ),
616        miette::Report,
617    > {
618        match self {
619            Self::Cedar(str) => crate::SchemaFragment::from_cedarschema_str(&str)
620                .map(|(sch, warnings)| {
621                    (
622                        sch,
623                        Box::new(warnings) as Box<dyn Iterator<Item = SchemaWarning>>,
624                    )
625                })
626                .wrap_err("failed to parse schema from string"),
627            Self::Json(val) => crate::SchemaFragment::from_json_value(val.into())
628                .map(|sch| {
629                    (
630                        sch,
631                        Box::new(std::iter::empty()) as Box<dyn Iterator<Item = SchemaWarning>>,
632                    )
633                })
634                .wrap_err("failed to parse schema from JSON"),
635        }
636    }
637}
638
639pub(super) struct WithWarnings<T> {
640    pub t: T,
641    pub warnings: Vec<miette::Report>,
642}
643
644/// Testing utilities used here and elsewhere
645// PANIC SAFETY unit tests
646#[allow(clippy::panic, clippy::indexing_slicing)]
647// Also disable some other clippy lints that are unimportant for testing code
648#[allow(clippy::module_name_repetitions, clippy::missing_panics_doc)]
649#[cfg(test)]
650pub mod test_utils {
651    use super::*;
652
653    /// Assert that an error has the specified message and help fields.
654    #[track_caller]
655    pub fn assert_error_matches(err: &DetailedError, msg: &str, help: Option<&str>) {
656        assert_eq!(err.message, msg, "did not see the expected error message");
657        assert_eq!(
658            err.help,
659            help.map(Into::into),
660            "did not see the expected help message"
661        );
662    }
663
664    /// Assert that a vector (of errors) has the expected length
665    #[track_caller]
666    pub fn assert_length_matches<T: std::fmt::Debug>(errs: &[T], n: usize) {
667        assert_eq!(
668            errs.len(),
669            n,
670            "expected {n} error(s) but saw {}",
671            errs.len()
672        );
673    }
674
675    /// Assert that a vector contains exactly one error with the specified
676    /// message and help text.
677    #[track_caller]
678    pub fn assert_exactly_one_error(errs: &[DetailedError], msg: &str, help: Option<&str>) {
679        assert_length_matches(errs, 1);
680        assert_error_matches(&errs[0], msg, help);
681    }
682}
683
684// PANIC SAFETY unit tests
685#[allow(clippy::panic, clippy::indexing_slicing)]
686// Also disable some other clippy lints that are unimportant for testing code
687#[allow(clippy::too_many_lines)]
688#[cfg(test)]
689mod test {
690    use super::*;
691    use cedar_policy_core::test_utils::*;
692    use serde_json::json;
693    use test_utils::assert_length_matches;
694
695    #[test]
696    fn test_policy_parser() {
697        // A string literal will be parsed as a policy in the Cedar syntax
698        let policy_json = json!("permit(principal == User::\"alice\", action, resource);");
699        let policy: Policy =
700            serde_json::from_value(policy_json).expect("failed to parse from JSON");
701        policy.parse(None).expect("failed to convert to policy");
702
703        // A JSON object will be parsed as a policy in the JSON syntax
704        let policy_json = json!({
705            "effect": "permit",
706            "principal": {
707                "op": "==",
708                "entity": { "type": "User", "id": "alice" }
709            },
710            "action": {
711                "op": "All"
712            },
713            "resource": {
714                "op": "All"
715            },
716            "conditions": []
717        });
718        let policy: Policy =
719            serde_json::from_value(policy_json).expect("failed to parse from JSON");
720        policy.parse(None).expect("failed to convert to policy");
721
722        // Invalid Cedar syntax
723        let src = "foo(principal == User::\"alice\", action, resource);";
724        let policy: Policy = serde_json::from_value(json!(src)).expect("failed to parse from JSON");
725        let err = policy
726            .parse(None)
727            .expect_err("should have failed to convert to policy");
728        expect_err(
729            src,
730            &err,
731            &ExpectedErrorMessageBuilder::error("failed to parse policy from string")
732                .source("invalid policy effect: foo")
733                .exactly_one_underline("foo")
734                .help("effect must be either `permit` or `forbid`")
735                .build(),
736        );
737
738        // Not a static policy
739        let src = "permit(principal == ?principal, action, resource);";
740        let policy: Policy =
741            serde_json::from_value(json!(src)).expect("failed to parse from string");
742        let err = policy
743            .parse(None)
744            .expect_err("should have failed to convert to policy");
745        expect_err(
746            src,
747            &err,
748            &ExpectedErrorMessageBuilder::error("failed to parse policy from string")
749                .source("expected a static policy, got a template containing the slot ?principal")
750                .exactly_one_underline("?principal")
751                .help("try removing the template slot(s) from this policy")
752                .build(),
753        );
754
755        // Not a single policy
756        let src = "permit(principal == User::\"alice\", action, resource); permit(principal == User::\"bob\", action, resource);";
757        let policy: Policy =
758            serde_json::from_value(json!(src)).expect("failed to parse from string");
759        let err = policy
760            .parse(None)
761            .expect_err("should have failed to convert to policy");
762        expect_err(
763            src,
764            &err,
765            &ExpectedErrorMessageBuilder::error("failed to parse policy from string")
766                .source("unexpected token `permit`")
767                .exactly_one_underline("permit")
768                .build(),
769        );
770
771        // Invalid JSON syntax (duplicate keys)
772        // The error message comes from the `serde(expecting = ..)` annotation on `Policy`
773        let policy_json_str = r#"{
774            "effect": "permit",
775            "effect": "forbid"
776        }"#;
777        let err = serde_json::from_str::<Policy>(policy_json_str)
778            .expect_err("should have failed to parse from JSON");
779        assert_eq!(
780            err.to_string(),
781            "expected a static policy in the Cedar or JSON policy format (with no duplicate keys)"
782        );
783    }
784
785    #[test]
786    fn test_template_parser() {
787        // A string literal will be parsed as a template in the Cedar syntax
788        let template_json = json!("permit(principal == ?principal, action, resource);");
789        let template: Template =
790            serde_json::from_value(template_json).expect("failed to parse from JSON");
791        template.parse(None).expect("failed to convert to template");
792
793        // A JSON object will be parsed as a template in the JSON syntax
794        let template_json = json!({
795            "effect": "permit",
796            "principal": {
797                "op": "==",
798                "slot": "?principal"
799            },
800            "action": {
801                "op": "All"
802            },
803            "resource": {
804                "op": "All"
805            },
806            "conditions": []
807        });
808        let template: Template =
809            serde_json::from_value(template_json).expect("failed to parse from JSON");
810        template.parse(None).expect("failed to convert to template");
811
812        // Invalid syntax
813        let src = "permit(principal == ?foo, action, resource);";
814        let template: Template =
815            serde_json::from_value(json!(src)).expect("failed to parse from JSON");
816        let err = template
817            .parse(None)
818            .expect_err("should have failed to convert to template");
819        expect_err(
820            src,
821            &err,
822            &ExpectedErrorMessageBuilder::error("failed to parse template from string")
823                .source("expected an entity uid or matching template slot, found ?foo instead of ?principal")
824                .exactly_one_underline("?foo")
825                .build(),
826        );
827
828        // Static policies cannot be parsed as templates
829        let src = "permit(principal == User::\"alice\", action, resource);";
830        let template: Template =
831            serde_json::from_value(json!(src)).expect("failed to parse from JSON");
832        let err = template
833            .parse(None)
834            .expect_err("should have failed to convert to template");
835        expect_err(
836            src,
837            &err,
838            &ExpectedErrorMessageBuilder::error("failed to parse template from string")
839                .source("expected a template, got a static policy")
840                .help("a template should include slot(s) `?principal` or `?resource`")
841                .exactly_one_underline(src)
842                .build(),
843        );
844    }
845
846    #[test]
847    fn test_static_policy_set_parser() {
848        // A string literal will be parsed as the `Concatenated` variant
849        let policies_json = json!("permit(principal == User::\"alice\", action, resource);");
850        let policies: StaticPolicySet =
851            serde_json::from_value(policies_json).expect("failed to parse from JSON");
852        policies
853            .parse()
854            .expect("failed to convert to static policy set");
855
856        // A JSON array will be parsed as the `Set` variant
857        let policies_json = json!([
858            {
859                "effect": "permit",
860                "principal": {
861                    "op": "==",
862                    "entity": { "type": "User", "id": "alice" }
863                },
864                "action": {
865                    "op": "All"
866                },
867                "resource": {
868                    "op": "All"
869                },
870                "conditions": []
871            },
872            "permit(principal == User::\"bob\", action, resource);"
873        ]);
874        let policies: StaticPolicySet =
875            serde_json::from_value(policies_json).expect("failed to parse from JSON");
876        policies
877            .parse()
878            .expect("failed to convert to static policy set");
879
880        // A JSON object will be parsed as the `Map` variant
881        let policies_json = json!({
882            "policy0": {
883                "effect": "permit",
884                "principal": {
885                    "op": "==",
886                    "entity": { "type": "User", "id": "alice" }
887                },
888                "action": {
889                    "op": "All"
890                },
891                "resource": {
892                    "op": "All"
893                },
894                "conditions": []
895            },
896            "policy1": "permit(principal == User::\"bob\", action, resource);"
897        });
898        let policies: StaticPolicySet =
899            serde_json::from_value(policies_json).expect("failed to parse from JSON");
900        policies
901            .parse()
902            .expect("failed to convert to static policy set");
903
904        // Invalid static policy set - `policy0` is a template
905        let policies_json = json!({
906            "policy0": "permit(principal == ?principal, action, resource);",
907            "policy1": "permit(principal == User::\"bob\", action, resource);"
908        });
909        let policies: StaticPolicySet =
910            serde_json::from_value(policies_json).expect("failed to parse from JSON");
911        let errs = policies
912            .parse()
913            .expect_err("should have failed to convert to static policy set");
914        assert_length_matches(&errs, 1);
915        expect_err(
916            "permit(principal == ?principal, action, resource);",
917            &errs[0],
918            &ExpectedErrorMessageBuilder::error(
919                "failed to parse policy with id `policy0` from string",
920            )
921            .source("expected a static policy, got a template containing the slot ?principal")
922            .exactly_one_underline("?principal")
923            .help("try removing the template slot(s) from this policy")
924            .build(),
925        );
926
927        // Invalid static policy set - the second policy is a template
928        let policies_json = json!(
929            "
930            permit(principal == User::\"alice\", action, resource);
931            permit(principal == ?principal, action, resource);
932        "
933        );
934        let policies: StaticPolicySet =
935            serde_json::from_value(policies_json).expect("failed to parse from JSON");
936        let errs = policies
937            .parse()
938            .expect_err("should have failed to convert to static policy set");
939        assert_length_matches(&errs, 1);
940        expect_err(
941            "permit(principal == ?principal, action, resource);",
942            &errs[0],
943            &ExpectedErrorMessageBuilder::error("static policy set includes a template").build(),
944        );
945
946        // Invalid static policy set - `policy1` is actually multiple policies
947        let policies_json = json!({
948            "policy0": "permit(principal == User::\"alice\", action, resource);",
949            "policy1": "permit(principal == User::\"bob\", action, resource); permit(principal, action, resource);"
950        });
951        let policies: StaticPolicySet =
952            serde_json::from_value(policies_json).expect("failed to parse from JSON");
953        let errs = policies
954            .parse()
955            .expect_err("should have failed to convert to static policy set");
956        assert_length_matches(&errs, 1);
957        expect_err(
958            "permit(principal == User::\"bob\", action, resource); permit(principal, action, resource);",
959            &errs[0],
960            &ExpectedErrorMessageBuilder::error(
961                "failed to parse policy with id `policy1` from string",
962            )
963            .source("unexpected token `permit`")
964            .exactly_one_underline("permit")
965            .build(),
966        );
967
968        // Invalid static policy set - both policies are ill-formed
969        let policies_json = json!({
970            "policy0": "permit(principal, action);",
971            "policy1": "forbid(principal, action);"
972        });
973        let policies: StaticPolicySet =
974            serde_json::from_value(policies_json).expect("failed to parse from JSON");
975        let errs = policies
976            .parse()
977            .expect_err("should have failed to convert to static policy set");
978        assert_length_matches(&errs, 2);
979        for err in errs {
980            // hack to account for nondeterministic error ordering
981            if err
982                .to_string()
983                .contains("failed to parse policy with id `policy0`")
984            {
985                expect_err(
986                "permit(principal, action);",
987                &err,
988                &ExpectedErrorMessageBuilder::error(
989                        "failed to parse policy with id `policy0` from string",
990                    )
991                    .source("this policy is missing the `resource` variable in the scope")
992                    .exactly_one_underline("")
993                    .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
994                    .build(),
995            );
996            } else {
997                expect_err(
998                "forbid(principal, action);",
999                &err,
1000                &ExpectedErrorMessageBuilder::error(
1001                        "failed to parse policy with id `policy1` from string",
1002                    )
1003                    .source("this policy is missing the `resource` variable in the scope")
1004                    .exactly_one_underline("")
1005                    .help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
1006                    .build(),
1007            );
1008            }
1009        }
1010    }
1011
1012    #[test]
1013    fn test_policy_set_parser() {
1014        // Empty policy set
1015        let policies_json = json!({});
1016        let policies: PolicySet =
1017            serde_json::from_value(policies_json).expect("failed to parse from JSON");
1018        policies.parse().expect("failed to convert to policy set");
1019
1020        // Example valid policy set
1021        let policies_json = json!({
1022            "staticPolicies": [
1023                {
1024                    "effect": "permit",
1025                    "principal": {
1026                        "op": "==",
1027                        "entity": { "type": "User", "id": "alice" }
1028                    },
1029                    "action": {
1030                        "op": "All"
1031                    },
1032                    "resource": {
1033                        "op": "All"
1034                    },
1035                    "conditions": []
1036                },
1037                "permit(principal == User::\"bob\", action, resource);"
1038            ],
1039            "templates": {
1040                "ID0": "permit(principal == ?principal, action, resource);"
1041            },
1042            "templateLinks": [
1043                {
1044                    "templateId": "ID0",
1045                    "newId": "ID1",
1046                    "values": { "?principal": { "type": "User", "id": "charlie" } }
1047                }
1048            ]
1049        });
1050        let policies: PolicySet =
1051            serde_json::from_value(policies_json).expect("failed to parse from JSON");
1052        policies.parse().expect("failed to convert to policy set");
1053
1054        // Example policy set with a link error - `policy0` is already used
1055        let policies_json = json!({
1056            "staticPolicies": {
1057                "policy0": "permit(principal == User::\"alice\", action, resource);",
1058                "policy1": "permit(principal == User::\"bob\", action, resource);"
1059            },
1060            "templates": {
1061                "template": "permit(principal == ?principal, action, resource);"
1062            },
1063            "templateLinks": [
1064                {
1065                    "templateId": "template",
1066                    "newId": "policy0",
1067                    "values": { "?principal": { "type": "User", "id": "charlie" } }
1068                }
1069            ]
1070        });
1071        let policies: PolicySet =
1072            serde_json::from_value(policies_json).expect("failed to parse from JSON");
1073        let errs = policies
1074            .parse()
1075            .expect_err("should have failed to convert to policy set");
1076        assert_length_matches(&errs, 1);
1077        expect_err(
1078            "",
1079            &errs[0],
1080            &ExpectedErrorMessageBuilder::error("unable to link template")
1081                .source("template-linked policy id `policy0` conflicts with an existing policy id")
1082                .build(),
1083        );
1084    }
1085
1086    #[test]
1087    fn policy_set_parser_is_compatible_with_est_parser() {
1088        // The `PolicySet::parse` function accepts the `est::PolicySet` JSON format
1089        let json = json!({
1090            "staticPolicies": {
1091                "policy1": {
1092                    "effect": "permit",
1093                    "principal": {
1094                        "op": "==",
1095                        "entity": { "type": "User", "id": "alice" }
1096                    },
1097                    "action": {
1098                        "op": "==",
1099                        "entity": { "type": "Action", "id": "view" }
1100                    },
1101                    "resource": {
1102                        "op": "in",
1103                        "entity": { "type": "Folder", "id": "foo" }
1104                    },
1105                    "conditions": []
1106                }
1107            },
1108            "templates": {
1109                "template": {
1110                    "effect" : "permit",
1111                    "principal" : {
1112                        "op" : "==",
1113                        "slot" : "?principal"
1114                    },
1115                    "action" : {
1116                        "op" : "all"
1117                    },
1118                    "resource" : {
1119                        "op" : "all",
1120                    },
1121                    "conditions": []
1122                }
1123            },
1124            "templateLinks" : [
1125                {
1126                    "newId" : "link",
1127                    "templateId" : "template",
1128                    "values" : {
1129                        "?principal" : { "type" : "User", "id" : "bob" }
1130                    }
1131                }
1132            ]
1133        });
1134
1135        // use `crate::PolicySet::from_json_value`
1136        let ast_from_est = crate::PolicySet::from_json_value(json.clone())
1137            .expect("failed to convert to policy set");
1138
1139        // use `PolicySet::parse`
1140        let ffi_policy_set: PolicySet =
1141            serde_json::from_value(json).expect("failed to parse from JSON");
1142        let ast_from_ffi = ffi_policy_set
1143            .parse()
1144            .expect("failed to convert to policy set");
1145
1146        // check that the produced policy sets match
1147        assert_eq!(ast_from_est, ast_from_ffi);
1148    }
1149
1150    #[test]
1151    fn test_schema_parser() {
1152        // A string literal will be parsed as a schema in the Cedar syntax
1153        let schema_json = json!("entity User = {name: String};\nentity Photo;\naction viewPhoto appliesTo {principal: User, resource: Photo};");
1154        let schema: Schema =
1155            serde_json::from_value(schema_json).expect("failed to parse from JSON");
1156        let _ = schema.parse().expect("failed to convert to schema");
1157
1158        // A JSON object will be parsed as a schema in the JSON syntax
1159        let schema_json = json!({
1160            "": {
1161                "entityTypes": {
1162                    "User": {
1163                        "shape": {
1164                            "type": "Record",
1165                            "attributes": {
1166                                "name": {
1167                                    "type": "String"
1168                                }
1169                            }
1170                        }
1171                    },
1172                    "Photo": {}
1173                },
1174                "actions": {
1175                    "viewPhoto": {
1176                        "appliesTo": {
1177                            "principalTypes": [ "User" ],
1178                            "resourceTypes": [ "Photo" ]
1179                        }
1180                    }
1181                }
1182            }
1183        });
1184        let schema: Schema =
1185            serde_json::from_value(schema_json).expect("failed to parse from JSON");
1186        let _ = schema.parse().expect("failed to convert to schema");
1187
1188        // Invalid syntax (the value is a policy)
1189        let src = "permit(principal == User::\"alice\", action, resource);";
1190        let schema: Schema = serde_json::from_value(json!(src)).expect("failed to parse from JSON");
1191        let err = schema
1192            .parse()
1193            .map(|(s, _)| s)
1194            .expect_err("should have failed to convert to schema");
1195        expect_err(
1196            src,
1197            &err,
1198            &ExpectedErrorMessageBuilder::error("failed to parse schema from string")
1199                .exactly_one_underline_with_label(
1200                    "permit",
1201                    "expected `@`, `action`, `entity`, `namespace`, or `type`",
1202                )
1203                .source("error parsing schema: unexpected token `permit`")
1204                .build(),
1205        );
1206    }
1207}