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::{
23 add_template_links_to_set, add_template_links_to_set_if_exists, read_from_file_or_stdin,
24};
25
26#[derive(Args, Debug)]
28pub struct PoliciesArgs {
29 #[arg(short, long = "policies", value_name = "FILE")]
31 pub policies_file: Option<String>,
32 #[arg(long = "policy-format", default_value_t, value_enum)]
34 pub policy_format: PolicyFormat,
35 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
37 pub template_linked_file: Option<String>,
38}
39
40impl PoliciesArgs {
41 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 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 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#[derive(Args, Debug)]
73pub struct OptionalPoliciesArgs {
74 #[arg(short, long = "policies", value_name = "FILE")]
76 pub policies_file: Option<String>,
77 #[arg(long = "policy-format", default_value_t, value_enum)]
79 pub policy_format: PolicyFormat,
80 #[arg(short = 'k', long = "template-linked", value_name = "FILE")]
83 pub template_linked_file: Option<String>,
84}
85
86impl OptionalPoliciesArgs {
87 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 #[default]
108 Cedar,
109 Json,
111}
112
113pub(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
132pub(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
192fn 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 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}