Skip to main content

cedar_policy_cli/utils/
policies.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
17use cedar_policy::{Policy, PolicySet, Template};
18use clap::{Args, ValueEnum};
19use miette::{miette, IntoDiagnostic, NamedSource, Report, Result, WrapErr};
20use std::{path::Path, str::FromStr};
21
22use crate::{
23    add_template_links_to_set, add_template_links_to_set_if_exists, read_from_file_or_stdin,
24};
25
26/// This struct contains the arguments that together specify an input policy or policy set.
27#[derive(Args, Debug)]
28pub struct PoliciesArgs {
29    /// File containing the static Cedar policies and/or templates. If not provided, read policies from stdin.
30    #[arg(short, long = "policies", value_name = "FILE")]
31    pub policies_file: Option<String>,
32    /// Format of policies in the `--policies` file
33    #[arg(long = "policy-format", default_value_t, value_enum)]
34    pub policy_format: PolicyFormat,
35    /// File containing template-linked policies
36    #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
37    pub template_linked_file: Option<String>,
38}
39
40impl PoliciesArgs {
41    /// Turn this `PoliciesArgs` into the appropriate `PolicySet` object
42    pub(crate) fn get_policy_set(&self) -> Result<PolicySet> {
43        let mut pset = self.get_static_policies_and_templates()?;
44        if let Some(links_filename) = self.template_linked_file.as_ref() {
45            add_template_links_to_set(links_filename, &mut pset)?;
46        }
47        Ok(pset)
48    }
49
50    /// Like `get_policy_set`, but allows a missing template-linked file. Used
51    /// by the `link` command which creates the file if needed.
52    pub(crate) fn get_policy_set_allow_missing_links(&self) -> Result<PolicySet> {
53        let mut pset = self.get_static_policies_and_templates()?;
54        if let Some(links_filename) = self.template_linked_file.as_ref() {
55            add_template_links_to_set_if_exists(links_filename, &mut pset)?;
56        }
57        Ok(pset)
58    }
59
60    /// Read static policies and templates  from `self.policies_file`.
61    /// Does _not_ load template links.
62    fn get_static_policies_and_templates(&self) -> Result<PolicySet> {
63        match self.policy_format {
64            PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
65            PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
66        }
67    }
68}
69
70/// This struct contains the arguments that together specify an input policy or policy set,
71/// for commands where policies are optional.
72#[derive(Args, Debug)]
73pub struct OptionalPoliciesArgs {
74    /// File containing static Cedar policies and/or templates
75    #[arg(short, long = "policies", value_name = "FILE")]
76    pub policies_file: Option<String>,
77    /// Format of policies in the `--policies` file
78    #[arg(long = "policy-format", default_value_t, value_enum)]
79    pub policy_format: PolicyFormat,
80    /// File containing template-linked policies. Ignored if `--policies` is not
81    /// present (because in that case there are no templates to link against)
82    #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
83    pub template_linked_file: Option<String>,
84}
85
86impl OptionalPoliciesArgs {
87    /// Turn this `OptionalPoliciesArgs` into the appropriate `PolicySet`
88    /// object, or `None` if no policies were provided
89    pub(crate) fn get_policy_set(&self) -> Result<Option<PolicySet>> {
90        match &self.policies_file {
91            None => Ok(None),
92            Some(policies_file) => {
93                let pargs = PoliciesArgs {
94                    policies_file: Some(policies_file.clone()),
95                    policy_format: self.policy_format,
96                    template_linked_file: self.template_linked_file.clone(),
97                };
98                pargs.get_policy_set().map(Some)
99            }
100        }
101    }
102}
103
104#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
105pub enum PolicyFormat {
106    /// The standard Cedar policy format, documented at <https://docs.cedarpolicy.com/policies/syntax-policy.html>
107    #[default]
108    Cedar,
109    /// Cedar's JSON policy format, documented at <https://docs.cedarpolicy.com/policies/json-format.html>
110    Json,
111}
112
113/// Read a policy set, in Cedar syntax, from the file given in `filename`,
114/// or from stdin if `filename` is `None`.
115pub(crate) fn read_cedar_policy_set(
116    filename: Option<impl AsRef<Path> + std::marker::Copy>,
117) -> Result<PolicySet> {
118    let context = "policy set";
119    let ps_str = read_from_file_or_stdin(filename.as_ref(), context)?;
120    let ps = PolicySet::from_str(&ps_str)
121        .wrap_err_with(|| format!("failed to parse {context}"))
122        .map_err(|err| {
123            let name = filename.map_or_else(
124                || "<stdin>".to_owned(),
125                |n| n.as_ref().display().to_string(),
126            );
127            err.with_source_code(NamedSource::new(name, ps_str))
128        })?;
129    rename_from_id_annotation(&ps)
130}
131
132/// Read a policy set, static policy or policy template, in Cedar JSON (EST) syntax, from the file given
133/// in `filename`, or from stdin if `filename` is `None`.
134pub(crate) fn read_json_policy_set(
135    filename: Option<impl AsRef<Path> + std::marker::Copy>,
136) -> Result<PolicySet> {
137    let context = "JSON policy";
138    let json_source = read_from_file_or_stdin(filename.as_ref(), context)?;
139    let json = serde_json::from_str::<serde_json::Value>(&json_source)
140        .into_diagnostic()
141        .wrap_err_with(|| format!("failed to parse {context}"))?;
142    let policy_type = get_json_policy_type(&json)?;
143
144    let add_json_source = |report: Report| {
145        let name = filename.map_or_else(
146            || "<stdin>".to_owned(),
147            |n| n.as_ref().display().to_string(),
148        );
149        report.with_source_code(NamedSource::new(name, json_source.clone()))
150    };
151
152    match policy_type {
153        JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
154            Ok(policy) => PolicySet::from_policies([policy])
155                .wrap_err_with(|| format!("failed to create policy set from {context}")),
156            Err(_) => {
157                let template = Template::from_json(None, json)
158                    .wrap_err_with(|| format!("failed to parse {context}"))
159                    .map_err(|err| add_json_source(err))?;
160                let mut ps = PolicySet::new();
161                ps.add_template(template)?;
162                Ok(ps)
163            }
164        },
165        JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
166            .wrap_err_with(|| format!("failed to create policy set from {context}"))
167            .map_err(|err| add_json_source(err)),
168    }
169}
170
171fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
172    let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
173    let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
174
175    let json_has_property = |p| json.get(p).is_some();
176    let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
177    let has_any_policy_property = policy_properties.iter().any(json_has_property);
178
179    match (has_any_policy_set_property, has_any_policy_property) {
180        (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
181        (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
182        (true, _) => Ok(JsonPolicyType::PolicySet),
183        (_, true) => Ok(JsonPolicyType::SinglePolicy),
184    }
185}
186
187enum JsonPolicyType {
188    SinglePolicy,
189    PolicySet,
190}
191
192/// Renames policies and templates based on (@id("new_id") annotation.
193/// If no such annotation exists, it keeps the current id.
194///
195/// This will rename template-linked policies to the id of their template, which may
196/// cause id conflicts, so only call this function before instancing
197/// templates into the policy set.
198fn rename_from_id_annotation(ps: &PolicySet) -> Result<PolicySet> {
199    let mut new_ps = PolicySet::new();
200    let t_iter = ps.templates().map(|t| match t.annotation("id") {
201        None => Ok(t.clone()),
202        Some(anno) => anno.parse().map(|a| t.new_id(a)),
203    });
204    for t in t_iter {
205        let template = t.unwrap_or_else(|never| match never {});
206        new_ps
207            .add_template(template)
208            .wrap_err("failed to add template to policy set")?;
209    }
210    let p_iter = ps.policies().map(|p| match p.annotation("id") {
211        None => Ok(p.clone()),
212        Some(anno) => anno.parse().map(|a| p.new_id(a)),
213    });
214    for p in p_iter {
215        let policy = p.unwrap_or_else(|never| match never {});
216        new_ps
217            .add(policy)
218            .wrap_err("failed to add template to policy set")?;
219    }
220    Ok(new_ps)
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::utils::test_utils::{render_err, TEMPFILE_FILTER};
227    use std::io::Write;
228
229    #[test]
230    fn cedar_policy_from_file_parse_error() {
231        let mut f = tempfile::NamedTempFile::new().unwrap();
232        f.write_all(b"not a valid policy").unwrap();
233        let err = read_cedar_policy_set(Some(f.path())).unwrap_err();
234        insta::with_settings!({filters => vec![TEMPFILE_FILTER]}, {
235            insta::assert_snapshot!(render_err(&err), @r"
236             × failed to parse policy set
237             ╰─▶ unexpected token `a`
238              ╭────
239            1 │ not a valid policy
240              ·     ┬
241              ·     ╰── expected `(`
242              ╰────
243            ");
244        });
245    }
246
247    #[test]
248    fn json_policy_from_file_invalid_json() {
249        let mut f = tempfile::NamedTempFile::new().unwrap();
250        f.write_all(b"not json at all").unwrap();
251        let err = read_json_policy_set(Some(f.path())).unwrap_err();
252        insta::with_settings!({filters => vec![TEMPFILE_FILTER]}, {
253            insta::assert_snapshot!(render_err(&err), @"
254            × failed to parse JSON policy
255            ╰─▶ expected ident at line 1 column 2
256            ");
257        });
258    }
259
260    #[test]
261    fn json_policy_from_file_bad_policy() {
262        let mut f = tempfile::NamedTempFile::new().unwrap();
263        // Valid JSON with policy properties, but invalid policy content —
264        // hits the Template::from_json fallback and the wrap_err "failed to parse" path
265        f.write_all(br#"{"effect":"permit","principal":{"op":"bogus"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[]}"#).unwrap();
266        let err = read_json_policy_set(Some(f.path())).unwrap_err();
267        insta::with_settings!({filters => vec![TEMPFILE_FILTER]}, {
268            insta::assert_snapshot!(render_err(&err), @r#"
269            × failed to parse JSON policy
270            ├─▶ error deserializing a policy/template from JSON
271            ╰─▶ unknown variant `bogus`, expected one of `All`, `all`, `==`, `in`, `is`
272            "#);
273        });
274    }
275}