1use 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::{add_template_links_to_set, read_from_file_or_stdin};
23
24#[derive(Args, Debug)]
26pub struct PoliciesArgs {
27 #[arg(short, long = "policies", value_name = "FILE")]
29 pub policies_file: Option<String>,
30 #[arg(long = "policy-format", default_value_t, value_enum)]
32 pub policy_format: PolicyFormat,
33 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
35 pub template_linked_file: Option<String>,
36}
37
38impl PoliciesArgs {
39 pub(crate) fn get_policy_set(&self) -> Result<PolicySet> {
41 let mut pset = match self.policy_format {
42 PolicyFormat::Cedar => read_cedar_policy_set(self.policies_file.as_ref()),
43 PolicyFormat::Json => read_json_policy_set(self.policies_file.as_ref()),
44 }?;
45 if let Some(links_filename) = self.template_linked_file.as_ref() {
46 add_template_links_to_set(links_filename, &mut pset)?;
47 }
48 Ok(pset)
49 }
50}
51
52#[derive(Args, Debug)]
55pub struct OptionalPoliciesArgs {
56 #[arg(short, long = "policies", value_name = "FILE")]
58 pub policies_file: Option<String>,
59 #[arg(long = "policy-format", default_value_t, value_enum)]
61 pub policy_format: PolicyFormat,
62 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
65 pub template_linked_file: Option<String>,
66}
67
68impl OptionalPoliciesArgs {
69 pub(crate) fn get_policy_set(&self) -> Result<Option<PolicySet>> {
72 match &self.policies_file {
73 None => Ok(None),
74 Some(policies_file) => {
75 let pargs = PoliciesArgs {
76 policies_file: Some(policies_file.clone()),
77 policy_format: self.policy_format,
78 template_linked_file: self.template_linked_file.clone(),
79 };
80 pargs.get_policy_set().map(Some)
81 }
82 }
83 }
84}
85
86#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
87pub enum PolicyFormat {
88 #[default]
90 Cedar,
91 Json,
93}
94
95pub(crate) fn read_cedar_policy_set(
98 filename: Option<impl AsRef<Path> + std::marker::Copy>,
99) -> Result<PolicySet> {
100 let context = "policy set";
101 let ps_str = read_from_file_or_stdin(filename.as_ref(), context)?;
102 let ps = PolicySet::from_str(&ps_str)
103 .map_err(|err| {
104 let name = filename.map_or_else(
105 || "<stdin>".to_owned(),
106 |n| n.as_ref().display().to_string(),
107 );
108 Report::new(err).with_source_code(NamedSource::new(name, ps_str))
109 })
110 .wrap_err_with(|| format!("failed to parse {context}"))?;
111 rename_from_id_annotation(&ps)
112}
113
114pub(crate) fn read_json_policy_set(
117 filename: Option<impl AsRef<Path> + std::marker::Copy>,
118) -> Result<PolicySet> {
119 let context = "JSON policy";
120 let json_source = read_from_file_or_stdin(filename.as_ref(), context)?;
121 let json = serde_json::from_str::<serde_json::Value>(&json_source).into_diagnostic()?;
122 let policy_type = get_json_policy_type(&json)?;
123
124 let add_json_source = |report: Report| {
125 let name = filename.map_or_else(
126 || "<stdin>".to_owned(),
127 |n| n.as_ref().display().to_string(),
128 );
129 report.with_source_code(NamedSource::new(name, json_source.clone()))
130 };
131
132 match policy_type {
133 JsonPolicyType::SinglePolicy => match Policy::from_json(None, json.clone()) {
134 Ok(policy) => PolicySet::from_policies([policy])
135 .wrap_err_with(|| format!("failed to create policy set from {context}")),
136 Err(_) => match Template::from_json(None, json)
137 .map_err(|err| add_json_source(Report::new(err)))
138 {
139 Ok(template) => {
140 let mut ps = PolicySet::new();
141 ps.add_template(template)?;
142 Ok(ps)
143 }
144 Err(err) => Err(err).wrap_err_with(|| format!("failed to parse {context}")),
145 },
146 },
147 JsonPolicyType::PolicySet => PolicySet::from_json_value(json)
148 .map_err(|err| add_json_source(Report::new(err)))
149 .wrap_err_with(|| format!("failed to create policy set from {context}")),
150 }
151}
152
153fn get_json_policy_type(json: &serde_json::Value) -> Result<JsonPolicyType> {
154 let policy_set_properties = ["staticPolicies", "templates", "templateLinks"];
155 let policy_properties = ["action", "effect", "principal", "resource", "conditions"];
156
157 let json_has_property = |p| json.get(p).is_some();
158 let has_any_policy_set_property = policy_set_properties.iter().any(json_has_property);
159 let has_any_policy_property = policy_properties.iter().any(json_has_property);
160
161 match (has_any_policy_set_property, has_any_policy_property) {
162 (false, false) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found no matching properties from either format")),
163 (true, true) => Err(miette!("cannot determine if json policy is a single policy or a policy set. Found matching properties from both formats")),
164 (true, _) => Ok(JsonPolicyType::PolicySet),
165 (_, true) => Ok(JsonPolicyType::SinglePolicy),
166 }
167}
168
169enum JsonPolicyType {
170 SinglePolicy,
171 PolicySet,
172}
173
174fn rename_from_id_annotation(ps: &PolicySet) -> Result<PolicySet> {
181 let mut new_ps = PolicySet::new();
182 let t_iter = ps.templates().map(|t| match t.annotation("id") {
183 None => Ok(t.clone()),
184 Some(anno) => anno.parse().map(|a| t.new_id(a)),
185 });
186 for t in t_iter {
187 let template = t.unwrap_or_else(|never| match never {});
188 new_ps
189 .add_template(template)
190 .wrap_err("failed to add template to policy set")?;
191 }
192 let p_iter = ps.policies().map(|p| match p.annotation("id") {
193 None => Ok(p.clone()),
194 Some(anno) => anno.parse().map(|a| p.new_id(a)),
195 });
196 for p in p_iter {
197 let policy = p.unwrap_or_else(|never| match never {});
198 new_ps
199 .add(policy)
200 .wrap_err("failed to add template to policy set")?;
201 }
202 Ok(new_ps)
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use crate::utils::test_utils::{render_err, TEMPFILE_FILTER};
209 use std::io::Write;
210
211 #[test]
212 fn cedar_policy_from_file_parse_error() {
213 let mut f = tempfile::NamedTempFile::new().unwrap();
214 f.write_all(b"not a valid policy").unwrap();
215 let err = read_cedar_policy_set(Some(f.path())).unwrap_err();
216 insta::with_settings!({filters => vec![TEMPFILE_FILTER]}, {
217 insta::assert_snapshot!(render_err(&err), @r"
218 × failed to parse policy set
219 ╰─▶ unexpected token `a`
220 ╭────
221 1 │ not a valid policy
222 · ┬
223 · ╰── expected `(`
224 ╰────
225 ");
226 });
227 }
228
229 #[test]
230 fn json_policy_from_file_invalid_json() {
231 let mut f = tempfile::NamedTempFile::new().unwrap();
232 f.write_all(b"not json at all").unwrap();
233 let err = read_json_policy_set(Some(f.path())).unwrap_err();
234 insta::with_settings!({filters => vec![TEMPFILE_FILTER]}, {
235 insta::assert_snapshot!(render_err(&err), @" × expected ident at line 1 column 2");
236 });
237 }
238
239 #[test]
240 fn json_policy_from_file_bad_policy() {
241 let mut f = tempfile::NamedTempFile::new().unwrap();
242 f.write_all(br#"{"effect":"permit","principal":{"op":"bogus"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[]}"#).unwrap();
245 let err = read_json_policy_set(Some(f.path())).unwrap_err();
246 insta::with_settings!({filters => vec![TEMPFILE_FILTER]}, {
247 insta::assert_snapshot!(render_err(&err), @r#"
248 × failed to parse JSON policy
249 ├─▶ error deserializing a policy/template from JSON
250 ╰─▶ unknown variant `bogus`, expected one of `All`, `all`, `==`, `in`, `is`
251 "#);
252 });
253 }
254}