use super::super::api;
use super::{ast::ProtobufConversionError, models, traits};
macro_rules! fallible_conversions {
( $A:ty, $A_expr:expr, $B:ty ) => {
impl From<&$A> for $B {
fn from(v: &$A) -> $B {
Self::from(&v.0)
}
}
impl TryFrom<$B> for $A {
type Error = ProtobufConversionError;
fn try_from(v: $B) -> Result<$A, Self::Error> {
Ok($A_expr(v.try_into()?))
}
}
};
}
fallible_conversions!(api::Entity, api::Entity, models::Entity);
fallible_conversions!(api::EntityUid, api::EntityUid, models::EntityUid);
fallible_conversions!(api::Entities, api::Entities, models::Entities);
fallible_conversions!(api::Schema, api::Schema, models::Schema);
fallible_conversions!(api::EntityTypeName, api::EntityTypeName, models::Name);
fallible_conversions!(api::EntityNamespace, api::EntityNamespace, models::Name);
fallible_conversions!(api::Expression, api::Expression, models::Expr);
fallible_conversions!(api::Request, api::Request, models::Request);
impl From<&api::Template> for models::TemplateBody {
fn from(v: &api::Template) -> Self {
Self::from(&v.ast)
}
}
impl TryFrom<models::TemplateBody> for api::Template {
type Error = ProtobufConversionError;
fn try_from(v: models::TemplateBody) -> Result<Self, Self::Error> {
Ok(Self::from_ast(v.try_into()?))
}
}
impl From<&api::Policy> for models::Policy {
fn from(v: &api::Policy) -> Self {
Self::from(&v.ast)
}
}
impl From<&api::PolicySet> for models::PolicySet {
fn from(v: &api::PolicySet) -> Self {
Self::from(&v.ast)
}
}
impl TryFrom<models::PolicySet> for api::PolicySet {
type Error = ProtobufConversionError;
fn try_from(v: models::PolicySet) -> Result<Self, Self::Error> {
let ast: cedar_policy_core::ast::PolicySet = v.try_into()?;
Self::from_ast(ast)
.map_err(|e| ProtobufConversionError::InvalidValue(format!("invalid policy set: {e}")))
}
}
macro_rules! standard_protobuf_impl {
( $api:ty, $model:ty ) => {
impl traits::Protobuf for $api {
fn encode(&self) -> Vec<u8> {
traits::encode_to_vec::<$model>(self)
}
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, traits::DecodeError> {
traits::try_decode::<$model, _, _>(buf)
}
}
};
}
standard_protobuf_impl!(api::Entity, models::Entity);
standard_protobuf_impl!(api::Entities, models::Entities);
standard_protobuf_impl!(api::Schema, models::Schema);
standard_protobuf_impl!(api::EntityTypeName, models::Name);
standard_protobuf_impl!(api::EntityNamespace, models::Name);
standard_protobuf_impl!(api::Template, models::TemplateBody);
standard_protobuf_impl!(api::Expression, models::Expr);
standard_protobuf_impl!(api::Request, models::Request);
impl traits::Protobuf for api::PolicySet {
fn encode(&self) -> Vec<u8> {
traits::encode_to_vec::<models::PolicySet>(self)
}
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, traits::DecodeError> {
traits::try_decode::<models::PolicySet, _, Self>(buf)
}
}
#[cfg(test)]
mod test {
use cool_asserts::assert_matches;
use prost::Message as _;
use std::{collections::HashMap, str::FromStr};
fn roundtrip_policies(policies: crate::PolicySet) {
let policies_proto = crate::proto::models::PolicySet::from(&policies);
let buf = policies_proto.encode_to_vec();
let roundtripped_proto = crate::proto::models::PolicySet::decode(&buf[..])
.expect("Failed to deserialize PolicySet from protobuf");
let roundtripped = crate::PolicySet::try_from(roundtripped_proto)
.expect("Failed to convert from protobuf to PolicySet");
similar_asserts::assert_eq!(policies, roundtripped);
}
fn roundtrip_policies_text(text: &str) {
let pset = crate::PolicySet::from_str(text).expect("Failed to parse policy set");
roundtrip_policies(pset);
}
#[test]
fn roundtrip_policyset_with_template_link() {
let mut pset = crate::PolicySet::from_str(
r#"
permit(principal == ?principal, action, resource);
"#,
)
.expect("Failed to parse policy set");
pset.link(
crate::PolicyId::new("policy0"),
crate::PolicyId::new("link0"),
HashMap::from([(
crate::SlotId::principal(),
crate::EntityUid::from_strs("User", "alice"),
)]),
)
.expect("Failed to link template");
roundtrip_policies(pset);
}
#[test]
fn roundtrip_policyset_empty() {
roundtrip_policies_text("");
}
#[test]
fn roundtrip_policyset_with_static_policy() {
roundtrip_policies_text(
r#"
permit(principal, action, resource);
"#,
);
}
#[test]
fn roundtrip_policyset_with_multiple_static_policies() {
roundtrip_policies_text(
r#"
permit(principal, action, resource);
forbid(principal, action, resource) when { context.is_restricted };
permit(principal == User::"alice", action == Action::"read", resource in Folder::"shared");
"#,
);
}
#[test]
fn roundtrip_policyset_with_when_and_unless() {
roundtrip_policies_text(
r#"
permit(principal, action, resource)
when { resource.owner == principal }
unless { principal.suspended };
"#,
);
}
#[test]
fn roundtrip_policyset_with_annotations() {
roundtrip_policies_text(
r#"
@advice("allow owner access")
permit(principal, action == Action::"write", resource)
when { resource.owner == principal };
"#,
);
}
#[test]
fn roundtrip_policyset_with_multiple_template_links() {
let mut pset = crate::PolicySet::from_str(
r#"
permit(principal == ?principal, action, resource in ?resource);
"#,
)
.expect("Failed to parse policy set");
pset.link(
crate::PolicyId::new("policy0"),
crate::PolicyId::new("link0"),
HashMap::from([
(
crate::SlotId::principal(),
crate::EntityUid::from_strs("User", "alice"),
),
(
crate::SlotId::resource(),
crate::EntityUid::from_strs("Folder", "shared"),
),
]),
)
.expect("Failed to link template");
pset.link(
crate::PolicyId::new("policy0"),
crate::PolicyId::new("link1"),
HashMap::from([
(
crate::SlotId::principal(),
crate::EntityUid::from_strs("User", "bob"),
),
(
crate::SlotId::resource(),
crate::EntityUid::from_strs("Folder", "private"),
),
]),
)
.expect("Failed to link template");
roundtrip_policies(pset);
}
#[test]
fn roundtrip_policyset_with_static_and_templates() {
let mut pset = crate::PolicySet::from_str(
r#"
forbid(principal, action, resource) unless { context.authenticated };
permit(principal == ?principal, action, resource);
"#,
)
.expect("Failed to parse policy set");
println!("{:?}", pset);
pset.link(
crate::PolicyId::new("policy1"),
crate::PolicyId::new("link0"),
HashMap::from([(
crate::SlotId::principal(),
crate::EntityUid::from_strs("User", "admin"),
)]),
)
.expect("Failed to link template");
roundtrip_policies(pset);
}
#[test]
fn roundtrip_policyset_with_is_constraint() {
roundtrip_policies_text(
r#"
permit(principal is User, action, resource is Folder);
"#,
);
}
#[test]
fn roundtrip_policyset_with_is_in_constraint() {
roundtrip_policies_text(
r#"
permit(principal is User in Group::"admins", action, resource);
"#,
);
}
#[test]
fn roundtrip_policyset_with_action_in_set() {
roundtrip_policies_text(
r#"
permit(principal, action in [Action::"read", Action::"list"], resource);
"#,
);
}
#[test]
fn roundtrip_policyset_with_extension_functions() {
roundtrip_policies_text(
r#"
forbid(principal, action, resource)
when { !context.src_ip.isInRange(ip("10.0.0.0/8")) };
"#,
);
}
#[test]
fn roundtrip_policyset_with_unlinked_template() {
roundtrip_policies_text(
r#"
permit(principal == ?principal, action, resource);
"#,
);
}
#[test]
fn decode_random_bytes_does_not_panic() {
use crate::proto::traits::Protobuf;
let inputs: &[&[u8]] = &[
b"",
b"\x00",
b"\xff\xff\xff\xff",
b"not a protobuf",
&[0u8; 1024],
&{
let mut v = Vec::new();
for i in 0u8..=255 {
v.push(i);
}
v
},
];
for input in inputs {
let _ = crate::Entity::decode(*input);
let _ = crate::Entities::decode(*input);
let _ = crate::Schema::decode(*input);
let _ = crate::EntityTypeName::decode(*input);
let _ = crate::EntityNamespace::decode(*input);
let _ = crate::Template::decode(*input);
let _ = crate::Expression::decode(*input);
let _ = crate::Request::decode(*input);
let _ = crate::PolicySet::decode(*input);
}
}
#[test]
fn decode_conversion_error_path() {
use crate::proto::traits::Protobuf;
let model = crate::proto::models::Entity {
uid: Some(crate::proto::models::EntityUid {
ty: Some(crate::proto::models::Name {
id: String::new(), path: vec![],
}),
eid: "x".to_string(),
}),
attrs: Default::default(),
ancestors: vec![],
tags: Default::default(),
};
let buf = prost::Message::encode_to_vec(&model);
assert_matches!(
crate::Entity::decode(&buf[..]),
Err(crate::proto::traits::DecodeError::Conversion(_))
);
}
}