use crate::{PolicyId, SchemaWarning, SlotId};
use miette::miette;
use miette::WrapErr;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
use std::{collections::HashMap, str::FromStr};
pub use cedar_policy_core::jsonvalue::JsonValueWithNoDuplicateKeys;
#[cfg(feature = "wasm")]
extern crate tsify;
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize, Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct DetailedError {
pub message: String,
pub help: Option<String>,
pub code: Option<String>,
pub url: Option<String>,
pub severity: Option<Severity>,
#[serde(default)]
pub source_locations: Vec<SourceLabel>,
#[serde(default)]
pub related: Vec<DetailedError>,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Deserialize, Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
pub enum Severity {
Advice,
Warning,
Error,
}
impl From<miette::Severity> for Severity {
fn from(severity: miette::Severity) -> Self {
match severity {
miette::Severity::Advice => Self::Advice,
miette::Severity::Warning => Self::Warning,
miette::Severity::Error => Self::Error,
}
}
}
#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize, Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct SourceLabel {
pub label: Option<String>,
#[serde(flatten)]
pub loc: SourceLocation,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, Deserialize, Serialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct SourceLocation {
pub start: usize,
pub end: usize,
}
impl From<miette::LabeledSpan> for SourceLabel {
fn from(span: miette::LabeledSpan) -> Self {
Self {
label: span.label().map(ToString::to_string),
loc: SourceLocation {
start: span.offset(),
end: span.offset() + span.len(),
},
}
}
}
impl<'a, E: miette::Diagnostic + ?Sized> From<&'a E> for DetailedError {
fn from(diag: &'a E) -> Self {
Self {
message: {
let mut s = diag.to_string();
let mut source = diag.source();
while let Some(e) = source {
s.push_str(": ");
s.push_str(&e.to_string());
source = e.source();
}
s
},
help: diag.help().map(|h| h.to_string()),
code: diag.code().map(|c| c.to_string()),
url: diag.url().map(|u| u.to_string()),
severity: diag.severity().map(Into::into),
source_locations: diag
.labels()
.map(|labels| labels.map(Into::into).collect())
.unwrap_or_default(),
related: diag
.related()
.map(|errs| errs.map(std::convert::Into::into).collect())
.unwrap_or_default(),
}
}
}
impl From<miette::Report> for DetailedError {
fn from(report: miette::Report) -> Self {
let diag: &dyn miette::Diagnostic = report.as_ref();
diag.into()
}
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
#[repr(transparent)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct EntityUid(
#[cfg_attr(feature = "wasm", tsify(type = "EntityUidJson"))] JsonValueWithNoDuplicateKeys,
);
impl EntityUid {
pub fn parse(self, category: Option<&str>) -> Result<crate::EntityUid, miette::Report> {
crate::EntityUid::from_json(self.0.into())
.wrap_err_with(|| format!("failed to parse {}", category.unwrap_or("entity uid")))
}
}
#[doc(hidden)]
impl From<serde_json::Value> for EntityUid {
fn from(json: serde_json::Value) -> Self {
Self(json.into())
}
}
#[derive(Debug, Serialize, Deserialize)]
#[repr(transparent)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Context(
#[cfg_attr(feature = "wasm", tsify(type = "Record<string, CedarValueJson>"))]
JsonValueWithNoDuplicateKeys,
);
impl Context {
pub fn parse(
self,
schema_ref: Option<&crate::Schema>,
action_ref: Option<&crate::EntityUid>,
) -> Result<crate::Context, miette::Report> {
crate::Context::from_json_value(
self.0.into(),
match (schema_ref, action_ref) {
(Some(s), Some(a)) => Some((s, a)),
_ => None,
},
)
.map_err(Into::into)
}
}
#[doc(hidden)]
impl From<serde_json::Value> for Context {
fn from(json: serde_json::Value) -> Self {
Self(json.into())
}
}
#[derive(Debug, Serialize, Deserialize)]
#[repr(transparent)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub struct Entities(
#[cfg_attr(feature = "wasm", tsify(type = "Array<EntityJson>"))] JsonValueWithNoDuplicateKeys,
);
impl Entities {
pub fn parse(
self,
opt_schema: Option<&crate::Schema>,
) -> Result<crate::Entities, miette::Report> {
crate::Entities::from_json_value(self.0.into(), opt_schema).map_err(Into::into)
}
}
#[doc(hidden)]
impl From<serde_json::Value> for Entities {
fn from(json: serde_json::Value) -> Self {
Self(json.into())
}
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(untagged)]
#[serde(
expecting = "expected a static policy in the Cedar or JSON policy format (with no duplicate keys)"
)]
pub enum Policy {
Cedar(String),
Json(#[cfg_attr(feature = "wasm", tsify(type = "PolicyJson"))] JsonValueWithNoDuplicateKeys),
}
impl Policy {
pub(super) fn parse(self, id: Option<PolicyId>) -> Result<crate::Policy, miette::Report> {
let msg = id
.clone()
.map_or(String::new(), |id| format!(" with id `{id}`"));
match self {
Self::Cedar(str) => crate::Policy::parse(id, str)
.wrap_err(format!("failed to parse policy{msg} from string")),
Self::Json(json) => crate::Policy::from_json(id, json.into())
.wrap_err(format!("failed to parse policy{msg} from JSON")),
}
}
pub fn get_valid_request_envs(
self,
s: Schema,
) -> Result<
(
impl Iterator<Item = String>,
impl Iterator<Item = String>,
impl Iterator<Item = String>,
),
miette::Report,
> {
let t = self.parse(None)?;
let (s, _) = s.parse()?;
let mut principals = BTreeSet::new();
let mut actions = BTreeSet::new();
let mut resources = BTreeSet::new();
for env in t.get_valid_request_envs(&s) {
principals.insert(env.principal.to_string());
actions.insert(env.action.to_string());
resources.insert(env.resource.to_string());
}
Ok((
principals.into_iter(),
actions.into_iter(),
resources.into_iter(),
))
}
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(untagged)]
#[serde(
expecting = "expected a policy template in the Cedar or JSON policy format (with no duplicate keys)"
)]
pub enum Template {
Cedar(String),
Json(#[cfg_attr(feature = "wasm", tsify(type = "PolicyJson"))] JsonValueWithNoDuplicateKeys),
}
impl Template {
pub(super) fn parse(self, id: Option<PolicyId>) -> Result<crate::Template, miette::Report> {
let msg = id
.clone()
.map(|id| format!(" with id `{id}`"))
.unwrap_or_default();
match self {
Self::Cedar(str) => crate::Template::parse(id, str)
.wrap_err(format!("failed to parse template{msg} from string")),
Self::Json(json) => crate::Template::from_json(id, json.into())
.wrap_err(format!("failed to parse template{msg} from JSON")),
}
}
pub(super) fn parse_and_add_to_set(
self,
id: Option<PolicyId>,
policies: &mut crate::PolicySet,
) -> Result<(), miette::Report> {
let msg = id
.clone()
.map(|id| format!(" with id `{id}`"))
.unwrap_or_default();
let template = self.parse(id)?;
policies
.add_template(template)
.wrap_err(format!("failed to add template{msg} to policy set"))
}
pub fn get_valid_request_envs(
self,
s: Schema,
) -> Result<
(
impl Iterator<Item = String>,
impl Iterator<Item = String>,
impl Iterator<Item = String>,
),
miette::Report,
> {
let t = self.parse(None)?;
let (s, _) = s.parse()?;
let mut principals = BTreeSet::new();
let mut actions = BTreeSet::new();
let mut resources = BTreeSet::new();
for env in t.get_valid_request_envs(&s) {
principals.insert(env.principal.to_string());
actions.insert(env.action.to_string());
resources.insert(env.resource.to_string());
}
Ok((
principals.into_iter(),
actions.into_iter(),
resources.into_iter(),
))
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(untagged)]
#[serde(
expecting = "expected a static policy set represented by a string, JSON array, or JSON object (with no duplicate keys)"
)]
pub enum StaticPolicySet {
Concatenated(String),
Set(Vec<Policy>),
#[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
Map(HashMap<PolicyId, Policy>),
}
impl StaticPolicySet {
pub(super) fn parse(self) -> Result<crate::PolicySet, Vec<miette::Report>> {
match self {
Self::Concatenated(str) => {
let policies = crate::PolicySet::from_str(&str)
.wrap_err("failed to parse policies from string")
.map_err(|e| vec![e])?;
if policies.templates().count() > 0 {
Err(vec![miette!("static policy set includes a template")])
} else {
Ok(policies)
}
}
Self::Set(set) => {
let mut errs = Vec::new();
let policies = set
.into_iter()
.map(|policy| policy.parse(None))
.filter_map(|r| r.map_err(|e| errs.push(e)).ok())
.collect::<Vec<_>>();
if errs.is_empty() {
crate::PolicySet::from_policies(policies).map_err(|e| vec![e.into()])
} else {
Err(errs)
}
}
Self::Map(map) => {
let mut errs = Vec::new();
let policies = map
.into_iter()
.map(|(id, policy)| policy.parse(Some(id)))
.filter_map(|r| r.map_err(|e| errs.push(e)).ok())
.collect::<Vec<_>>();
if errs.is_empty() {
crate::PolicySet::from_policies(policies).map_err(|e| vec![e.into()])
} else {
Err(errs)
}
}
}
}
}
impl Default for StaticPolicySet {
fn default() -> Self {
Self::Set(Vec::new())
}
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct TemplateLink {
template_id: PolicyId,
new_id: PolicyId,
#[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
values: HashMap<SlotId, EntityUid>,
}
impl TemplateLink {
pub(super) fn parse_and_add_to_set(
self,
policies: &mut crate::PolicySet,
) -> Result<(), miette::Report> {
let values: HashMap<_, _> = self
.values
.into_iter()
.map(|(slot, euid)| euid.parse(None).map(|euid| (slot, euid)))
.collect::<Result<HashMap<_, _>, _>>()
.wrap_err("failed to parse link values")?;
policies
.link(self.template_id, self.new_id, values)
.map_err(miette::Report::new)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
#[serde(deny_unknown_fields)]
pub struct PolicySet {
#[serde(default)]
static_policies: StaticPolicySet,
#[serde(with = "::serde_with::rust::maps_duplicate_key_is_error")]
#[serde(default)]
templates: HashMap<PolicyId, Template>,
#[serde(default)]
template_links: Vec<TemplateLink>,
}
impl PolicySet {
pub(super) fn parse(self) -> Result<crate::PolicySet, Vec<miette::Report>> {
let mut errs = Vec::new();
let mut policies = self.static_policies.parse().unwrap_or_else(|mut e| {
errs.append(&mut e);
crate::PolicySet::new()
});
self.templates.into_iter().for_each(|(id, template)| {
template
.parse_and_add_to_set(Some(id), &mut policies)
.unwrap_or_else(|e| errs.push(e));
});
self.template_links.into_iter().for_each(|link| {
link.parse_and_add_to_set(&mut policies)
.unwrap_or_else(|e| errs.push(e));
});
if !errs.is_empty() {
return Err(errs);
}
Ok(policies)
}
#[cfg(test)]
pub(super) fn new() -> Self {
Self {
static_policies: StaticPolicySet::Set(Vec::new()),
templates: HashMap::new(),
template_links: Vec::new(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(untagged)]
#[serde(
expecting = "expected a schema in the Cedar or JSON policy format (with no duplicate keys)"
)]
pub enum Schema {
Cedar(String),
Json(
#[cfg_attr(feature = "wasm", tsify(type = "SchemaJson<string>"))]
JsonValueWithNoDuplicateKeys,
),
}
impl Schema {
pub(super) fn parse(
self,
) -> Result<(crate::Schema, Box<dyn Iterator<Item = SchemaWarning>>), miette::Report> {
let (schema_frag, warnings) = self.parse_schema_fragment()?;
Ok((schema_frag.try_into()?, warnings))
}
pub(super) fn parse_schema_fragment(
self,
) -> Result<
(
crate::SchemaFragment,
Box<dyn Iterator<Item = SchemaWarning>>,
),
miette::Report,
> {
match self {
Self::Cedar(str) => crate::SchemaFragment::from_cedarschema_str(&str)
.map(|(sch, warnings)| {
(
sch,
Box::new(warnings) as Box<dyn Iterator<Item = SchemaWarning>>,
)
})
.wrap_err("failed to parse schema from string"),
Self::Json(val) => crate::SchemaFragment::from_json_value(val.into())
.map(|sch| {
(
sch,
Box::new(std::iter::empty()) as Box<dyn Iterator<Item = SchemaWarning>>,
)
})
.wrap_err("failed to parse schema from JSON"),
}
}
}
pub(super) struct WithWarnings<T> {
pub t: T,
pub warnings: Vec<miette::Report>,
}
#[allow(clippy::panic, clippy::indexing_slicing)]
#[allow(clippy::module_name_repetitions, clippy::missing_panics_doc)]
#[cfg(test)]
pub mod test_utils {
use super::*;
#[track_caller]
pub fn assert_error_matches(err: &DetailedError, msg: &str, help: Option<&str>) {
assert_eq!(err.message, msg, "did not see the expected error message");
assert_eq!(
err.help,
help.map(Into::into),
"did not see the expected help message"
);
}
#[track_caller]
pub fn assert_length_matches<T: std::fmt::Debug>(errs: &[T], n: usize) {
assert_eq!(
errs.len(),
n,
"expected {n} error(s) but saw {}",
errs.len()
);
}
#[track_caller]
pub fn assert_exactly_one_error(errs: &[DetailedError], msg: &str, help: Option<&str>) {
assert_length_matches(errs, 1);
assert_error_matches(&errs[0], msg, help);
}
}
#[allow(clippy::panic, clippy::indexing_slicing)]
#[allow(clippy::too_many_lines)]
#[cfg(test)]
mod test {
use super::*;
use cedar_policy_core::test_utils::*;
use serde_json::json;
use test_utils::assert_length_matches;
#[test]
fn test_policy_parser() {
let policy_json = json!("permit(principal == User::\"alice\", action, resource);");
let policy: Policy =
serde_json::from_value(policy_json).expect("failed to parse from JSON");
policy.parse(None).expect("failed to convert to policy");
let policy_json = json!({
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "alice" }
},
"action": {
"op": "All"
},
"resource": {
"op": "All"
},
"conditions": []
});
let policy: Policy =
serde_json::from_value(policy_json).expect("failed to parse from JSON");
policy.parse(None).expect("failed to convert to policy");
let src = "foo(principal == User::\"alice\", action, resource);";
let policy: Policy = serde_json::from_value(json!(src)).expect("failed to parse from JSON");
let err = policy
.parse(None)
.expect_err("should have failed to convert to policy");
expect_err(
src,
&err,
&ExpectedErrorMessageBuilder::error("failed to parse policy from string")
.source("invalid policy effect: foo")
.exactly_one_underline("foo")
.help("effect must be either `permit` or `forbid`")
.build(),
);
let src = "permit(principal == ?principal, action, resource);";
let policy: Policy =
serde_json::from_value(json!(src)).expect("failed to parse from string");
let err = policy
.parse(None)
.expect_err("should have failed to convert to policy");
expect_err(
src,
&err,
&ExpectedErrorMessageBuilder::error("failed to parse policy from string")
.source("expected a static policy, got a template containing the slot ?principal")
.exactly_one_underline(src)
.help("try removing the template slot(s) from this policy")
.build(),
);
let src = "permit(principal == User::\"alice\", action, resource); permit(principal == User::\"bob\", action, resource);";
let policy: Policy =
serde_json::from_value(json!(src)).expect("failed to parse from string");
let err = policy
.parse(None)
.expect_err("should have failed to convert to policy");
expect_err(
src,
&err,
&ExpectedErrorMessageBuilder::error("failed to parse policy from string")
.source("unexpected token `permit`")
.exactly_one_underline("permit")
.build(),
);
let policy_json_str = r#"{
"effect": "permit",
"effect": "forbid"
}"#;
let err = serde_json::from_str::<Policy>(policy_json_str)
.expect_err("should have failed to parse from JSON");
assert_eq!(
err.to_string(),
"expected a static policy in the Cedar or JSON policy format (with no duplicate keys)"
);
}
#[test]
fn test_template_parser() {
let template_json = json!("permit(principal == ?principal, action, resource);");
let template: Template =
serde_json::from_value(template_json).expect("failed to parse from JSON");
template.parse(None).expect("failed to convert to template");
let template_json = json!({
"effect": "permit",
"principal": {
"op": "==",
"slot": "?principal"
},
"action": {
"op": "All"
},
"resource": {
"op": "All"
},
"conditions": []
});
let template: Template =
serde_json::from_value(template_json).expect("failed to parse from JSON");
template.parse(None).expect("failed to convert to template");
let src = "permit(principal == ?foo, action, resource);";
let template: Template =
serde_json::from_value(json!(src)).expect("failed to parse from JSON");
let err = template
.parse(None)
.expect_err("should have failed to convert to template");
expect_err(
src,
&err,
&ExpectedErrorMessageBuilder::error("failed to parse template from string")
.source("expected an entity uid or matching template slot, found ?foo instead of ?principal")
.exactly_one_underline("?foo")
.build(),
);
let src = "permit(principal == User::\"alice\", action, resource);";
let template: Template =
serde_json::from_value(json!(src)).expect("failed to parse from JSON");
let err = template
.parse(None)
.expect_err("should have failed to convert to template");
expect_err(
src,
&err,
&ExpectedErrorMessageBuilder::error("failed to parse template from string")
.source("expected a template, got a static policy")
.help("a template should include slot(s) `?principal` or `?resource`")
.exactly_one_underline(src)
.build(),
);
}
#[test]
fn test_static_policy_set_parser() {
let policies_json = json!("permit(principal == User::\"alice\", action, resource);");
let policies: StaticPolicySet =
serde_json::from_value(policies_json).expect("failed to parse from JSON");
policies
.parse()
.expect("failed to convert to static policy set");
let policies_json = json!([
{
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "alice" }
},
"action": {
"op": "All"
},
"resource": {
"op": "All"
},
"conditions": []
},
"permit(principal == User::\"bob\", action, resource);"
]);
let policies: StaticPolicySet =
serde_json::from_value(policies_json).expect("failed to parse from JSON");
policies
.parse()
.expect("failed to convert to static policy set");
let policies_json = json!({
"policy0": {
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "alice" }
},
"action": {
"op": "All"
},
"resource": {
"op": "All"
},
"conditions": []
},
"policy1": "permit(principal == User::\"bob\", action, resource);"
});
let policies: StaticPolicySet =
serde_json::from_value(policies_json).expect("failed to parse from JSON");
policies
.parse()
.expect("failed to convert to static policy set");
let policies_json = json!({
"policy0": "permit(principal == ?principal, action, resource);",
"policy1": "permit(principal == User::\"bob\", action, resource);"
});
let policies: StaticPolicySet =
serde_json::from_value(policies_json).expect("failed to parse from JSON");
let errs = policies
.parse()
.expect_err("should have failed to convert to static policy set");
assert_length_matches(&errs, 1);
expect_err(
"permit(principal == ?principal, action, resource);",
&errs[0],
&ExpectedErrorMessageBuilder::error(
"failed to parse policy with id `policy0` from string",
)
.source("expected a static policy, got a template containing the slot ?principal")
.exactly_one_underline("permit(principal == ?principal, action, resource);")
.help("try removing the template slot(s) from this policy")
.build(),
);
let policies_json = json!(
"
permit(principal == User::\"alice\", action, resource);
permit(principal == ?principal, action, resource);
"
);
let policies: StaticPolicySet =
serde_json::from_value(policies_json).expect("failed to parse from JSON");
let errs = policies
.parse()
.expect_err("should have failed to convert to static policy set");
assert_length_matches(&errs, 1);
expect_err(
"permit(principal == ?principal, action, resource);",
&errs[0],
&ExpectedErrorMessageBuilder::error("static policy set includes a template").build(),
);
let policies_json = json!({
"policy0": "permit(principal == User::\"alice\", action, resource);",
"policy1": "permit(principal == User::\"bob\", action, resource); permit(principal, action, resource);"
});
let policies: StaticPolicySet =
serde_json::from_value(policies_json).expect("failed to parse from JSON");
let errs = policies
.parse()
.expect_err("should have failed to convert to static policy set");
assert_length_matches(&errs, 1);
expect_err(
"permit(principal == User::\"bob\", action, resource); permit(principal, action, resource);",
&errs[0],
&ExpectedErrorMessageBuilder::error(
"failed to parse policy with id `policy1` from string",
)
.source("unexpected token `permit`")
.exactly_one_underline("permit")
.build(),
);
let policies_json = json!({
"policy0": "permit(principal, action);",
"policy1": "forbid(principal, action);"
});
let policies: StaticPolicySet =
serde_json::from_value(policies_json).expect("failed to parse from JSON");
let errs = policies
.parse()
.expect_err("should have failed to convert to static policy set");
assert_length_matches(&errs, 2);
for err in errs {
if err
.to_string()
.contains("failed to parse policy with id `policy0`")
{
expect_err(
"permit(principal, action);",
&err,
&ExpectedErrorMessageBuilder::error(
"failed to parse policy with id `policy0` from string",
)
.source("this policy is missing the `resource` variable in the scope")
.exactly_one_underline("")
.help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
.build(),
);
} else {
expect_err(
"forbid(principal, action);",
&err,
&ExpectedErrorMessageBuilder::error(
"failed to parse policy with id `policy1` from string",
)
.source("this policy is missing the `resource` variable in the scope")
.exactly_one_underline("")
.help("policy scopes must contain a `principal`, `action`, and `resource` element in that order")
.build(),
);
}
}
}
#[test]
fn test_policy_set_parser() {
let policies_json = json!({});
let policies: PolicySet =
serde_json::from_value(policies_json).expect("failed to parse from JSON");
policies.parse().expect("failed to convert to policy set");
let policies_json = json!({
"staticPolicies": [
{
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "alice" }
},
"action": {
"op": "All"
},
"resource": {
"op": "All"
},
"conditions": []
},
"permit(principal == User::\"bob\", action, resource);"
],
"templates": {
"ID0": "permit(principal == ?principal, action, resource);"
},
"templateLinks": [
{
"templateId": "ID0",
"newId": "ID1",
"values": { "?principal": { "type": "User", "id": "charlie" } }
}
]
});
let policies: PolicySet =
serde_json::from_value(policies_json).expect("failed to parse from JSON");
policies.parse().expect("failed to convert to policy set");
let policies_json = json!({
"staticPolicies": {
"policy0": "permit(principal == User::\"alice\", action, resource);",
"policy1": "permit(principal == User::\"bob\", action, resource);"
},
"templates": {
"template": "permit(principal == ?principal, action, resource);"
},
"templateLinks": [
{
"templateId": "template",
"newId": "policy0",
"values": { "?principal": { "type": "User", "id": "charlie" } }
}
]
});
let policies: PolicySet =
serde_json::from_value(policies_json).expect("failed to parse from JSON");
let errs = policies
.parse()
.expect_err("should have failed to convert to policy set");
assert_length_matches(&errs, 1);
expect_err(
"",
&errs[0],
&ExpectedErrorMessageBuilder::error("unable to link template")
.source("template-linked policy id `policy0` conflicts with an existing policy id")
.build(),
);
}
#[test]
fn policy_set_parser_is_compatible_with_est_parser() {
let json = json!({
"staticPolicies": {
"policy1": {
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "alice" }
},
"action": {
"op": "==",
"entity": { "type": "Action", "id": "view" }
},
"resource": {
"op": "in",
"entity": { "type": "Folder", "id": "foo" }
},
"conditions": []
}
},
"templates": {
"template": {
"effect" : "permit",
"principal" : {
"op" : "==",
"slot" : "?principal"
},
"action" : {
"op" : "all"
},
"resource" : {
"op" : "all",
},
"conditions": []
}
},
"templateLinks" : [
{
"newId" : "link",
"templateId" : "template",
"values" : {
"?principal" : { "type" : "User", "id" : "bob" }
}
}
]
});
let ast_from_est = crate::PolicySet::from_json_value(json.clone())
.expect("failed to convert to policy set");
let ffi_policy_set: PolicySet =
serde_json::from_value(json).expect("failed to parse from JSON");
let ast_from_ffi = ffi_policy_set
.parse()
.expect("failed to convert to policy set");
assert_eq!(ast_from_est, ast_from_ffi);
}
#[test]
fn test_schema_parser() {
let schema_json = json!("entity User = {name: String};\nentity Photo;\naction viewPhoto appliesTo {principal: User, resource: Photo};");
let schema: Schema =
serde_json::from_value(schema_json).expect("failed to parse from JSON");
let _ = schema.parse().expect("failed to convert to schema");
let schema_json = json!({
"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {
"name": {
"type": "String"
}
}
}
},
"Photo": {}
},
"actions": {
"viewPhoto": {
"appliesTo": {
"principalTypes": [ "User" ],
"resourceTypes": [ "Photo" ]
}
}
}
}
});
let schema: Schema =
serde_json::from_value(schema_json).expect("failed to parse from JSON");
let _ = schema.parse().expect("failed to convert to schema");
let src = "permit(principal == User::\"alice\", action, resource);";
let schema: Schema = serde_json::from_value(json!(src)).expect("failed to parse from JSON");
let err = schema
.parse()
.map(|(s, _)| s)
.expect_err("should have failed to convert to schema");
expect_err(
src,
&err,
&ExpectedErrorMessageBuilder::error("failed to parse schema from string")
.exactly_one_underline_with_label(
"permit",
"expected `action`, `entity`, `namespace`, or `type`",
)
.source("error parsing schema: unexpected token `permit`")
.build(),
);
}
}