Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose protobufs in public API #1345

Merged
merged 4 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions cedar-policy-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ wasm-bindgen = { version = "0.2.82", optional = true }
chrono = { version = "0.4.38", optional = true, default-features = false}

# protobuf dependency
prost = { version = "0.13.3", optional = true }
prost = { version = "0.13", optional = true }

[features]
# by default, enable all Cedar extensions
Expand All @@ -64,7 +64,7 @@ protobufs = ["dep:prost", "dep:prost-build"]
[build-dependencies]
lalrpop = "0.22.0"
# protobuf dependency
prost-build = { version = "0.13.3", optional = true }
prost-build = { version = "0.13", optional = true }

[dev-dependencies]
cool_asserts = "2.0"
Expand Down
9 changes: 9 additions & 0 deletions cedar-policy-core/src/ast/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,15 @@ impl From<&proto::LiteralPolicy> for LiteralPolicy {
}
}

#[cfg(feature = "protobufs")]
impl TryFrom<&proto::LiteralPolicy> for Policy {
type Error = ReificationError;
fn try_from(policy: &proto::LiteralPolicy) -> Result<Self, Self::Error> {
// TODO: do we need to provide a nonempty `templates` argument to `.reify()`
LiteralPolicy::from(policy).reify(&HashMap::new())
}
}

#[cfg(feature = "protobufs")]
impl From<&LiteralPolicy> for proto::LiteralPolicy {
fn from(v: &LiteralPolicy) -> Self {
Expand Down
5 changes: 5 additions & 0 deletions cedar-policy-core/src/ast/policy_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,11 @@ impl PolicySet {
self.links.values()
}

/// Consume the `PolicySet`, producing an iterator of all the policies in it
pub fn into_policies(self) -> impl Iterator<Item = Policy> {
self.links.into_values()
}

/// Iterate over everything stored as template, including static policies.
/// Ie: all_templates() should equal templates() ++ static_policies().map(|p| p.template())
pub fn all_templates(&self) -> impl Iterator<Item = &Template> {
Expand Down
4 changes: 2 additions & 2 deletions cedar-policy-validator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ tsify = { version = "0.4.5", optional = true }
wasm-bindgen = { version = "0.2.82", optional = true }

# protobuf dependency
prost = { version = "0.13.3", optional = true }
prost = { version = "0.13", optional = true }

[features]
# by default, enable all Cedar extensions
Expand Down Expand Up @@ -63,7 +63,7 @@ miette = { version = "7.1.0", features = ["fancy"] }
[build-dependencies]
lalrpop = "0.22.0"
# protobuf dependency
prost-build = { version = "0.13.3", optional = true }
prost-build = { version = "0.13", optional = true }

[lints]
workspace = true
4 changes: 2 additions & 2 deletions cedar-policy-validator/protobuf_schema/Validator.proto
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@ message EntityRecordKind {
}

enum OpenTag {
OpenAttributes = 0;
ClosedAttributes = 1;
OpenAttributes = 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A general note about the protobuf impl. Does this mean that OpenAttributes on records in the schema would become a publicly accessible feature when we stabilize protobuf? What other internal details might be exposed this way?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting question.

Protobuf is not self-describing. In this PR, public-API users only see a blob of bytes, and if one of those bits indicates open-attributes vs closed-attributes, they would never know. But if anyone actually relies on our .proto files to encode/decode themselves, then yes, they see these attributes.

I believe the Lean protobuf parser errors if it sees OpenAttributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Arguably if we want to stabilize protobufs before OpenAttributes, we should remove OpenAttributes from the protobuf format.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'd agree with that. And also

ClosedAttributes = 1;
}

message Attributes {
Expand Down
3 changes: 2 additions & 1 deletion cedar-policy/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ smol_str = { version = "0.3", features = ["serde"] }
dhat = { version = "0.3.2", optional = true }
serde_with = "3.3.0"
nonempty = "0.10"
prost = { version = "0.13", optional = true }

# wasm dependencies
serde-wasm-bindgen = { version = "0.6", optional = true }
Expand Down Expand Up @@ -52,7 +53,7 @@ entity-manifest = ["cedar-policy-validator/entity-manifest"]
partial-eval = ["cedar-policy-core/partial-eval", "cedar-policy-validator/partial-eval"]
permissive-validate = []
partial-validate = ["cedar-policy-validator/partial-validate"]
protobufs = ["cedar-policy-validator/protobufs", "cedar-policy-core/protobufs"]
protobufs = ["dep:prost", "cedar-policy-validator/protobufs", "cedar-policy-core/protobufs"]
level-validate = ["cedar-policy-validator/level-validate"]
wasm = ["serde-wasm-bindgen", "tsify", "wasm-bindgen"]

Expand Down
170 changes: 169 additions & 1 deletion cedar-policy/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,55 @@ pub(crate) mod version {
}
}

/// Private functions to support implementing the `Protobuf` trait on various types
#[cfg(feature = "protobufs")]
mod proto {
use std::default::Default;

/// Encode `thing` into `buf` using the protobuf format `M`
///
/// `Err` is only returned if `buf` has insufficient space.
#[allow(dead_code)] // experimental feature, we might have use for this one in the future
pub(super) fn encode<M: prost::Message>(
thing: impl Into<M>,
buf: &mut impl prost::bytes::BufMut,
) -> Result<(), prost::EncodeError> {
thing.into().encode(buf)
}

/// Encode `thing` into a freshly-allocated buffer using the protobuf format `M`
pub(super) fn encode_to_vec<M: prost::Message>(thing: impl Into<M>) -> Vec<u8> {
thing.into().encode_to_vec()
}

/// Decode something of type `T` from `buf` using the protobuf format `M`
pub(super) fn decode<M: prost::Message + Default, T: for<'a> From<&'a M>>(
buf: impl prost::bytes::Buf,
) -> Result<T, prost::DecodeError> {
M::decode(buf).map(|m| T::from(&m))
}

/// Decode something of type `T` from `buf` using the protobuf format `M`
pub(super) fn try_decode<
M: prost::Message + Default,
E,
T: for<'a> TryFrom<&'a M, Error = E>,
>(
buf: impl prost::bytes::Buf,
) -> Result<Result<T, E>, prost::DecodeError> {
M::decode(buf).map(|m| T::try_from(&m))
}
}

/// Trait allowing serializing and deserializing in protobuf format
#[cfg(feature = "protobufs")]
pub trait Protobuf: Sized {
/// Encode into protobuf format. Returns a freshly-allocated buffer containing binary data.
fn encode(&self) -> Vec<u8>;
/// Decode the binary data in `buf`, producing something of type `Self`
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, prost::DecodeError>;
}

/// Entity datatype
#[repr(transparent)]
#[derive(Debug, Clone, PartialEq, Eq, RefCast, Hash)]
Expand Down Expand Up @@ -314,6 +363,16 @@ impl std::fmt::Display for Entity {
}
}

#[cfg(feature = "protobufs")]
impl Protobuf for Entity {
fn encode(&self) -> Vec<u8> {
proto::encode_to_vec::<ast::proto::Entity>(&self.0)
}
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, prost::DecodeError> {
proto::decode::<ast::proto::Entity, _>(buf).map(Self)
}
}

/// Represents an entity hierarchy, and allows looking up `Entity` objects by
/// Uid.
#[repr(transparent)]
Expand Down Expand Up @@ -763,6 +822,16 @@ impl IntoIterator for Entities {
}
}

#[cfg(feature = "protobufs")]
impl Protobuf for Entities {
fn encode(&self) -> Vec<u8> {
proto::encode_to_vec::<ast::proto::Entities>(&self.0)
}
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, prost::DecodeError> {
proto::decode::<ast::proto::Entities, _>(buf).map(Self)
}
}

/// Authorizer object, which provides responses to authorization queries
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
Expand Down Expand Up @@ -1637,6 +1706,16 @@ impl Schema {
}
}

#[cfg(feature = "protobufs")]
impl Protobuf for Schema {
fn encode(&self) -> Vec<u8> {
proto::encode_to_vec::<cedar_policy_validator::proto::ValidatorSchema>(&self.0)
}
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, prost::DecodeError> {
proto::decode::<cedar_policy_validator::proto::ValidatorSchema, _>(buf).map(Self)
}
}

/// Contains the result of policy validation.
///
/// The result includes the list of issues found by validation and whether validation succeeds or fails.
Expand Down Expand Up @@ -1831,6 +1910,16 @@ impl std::fmt::Display for EntityNamespace {
}
}

#[cfg(feature = "protobufs")]
impl Protobuf for EntityNamespace {
fn encode(&self) -> Vec<u8> {
proto::encode_to_vec::<ast::proto::Name>(&self.0)
}
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, prost::DecodeError> {
proto::decode::<ast::proto::Name, _>(buf).map(Self)
}
}

/// Represents a set of `Policy`s
#[derive(Debug, Clone, Default)]
pub struct PolicySet {
Expand Down Expand Up @@ -1929,6 +2018,12 @@ impl PolicySet {
})
}

/// Build the [`PolicySet`] from just the AST information
#[cfg_attr(not(feature = "protobufs"), allow(dead_code))]
fn from_ast(ast: ast::PolicySet) -> Result<Self, PolicySetError> {
Self::from_policies(ast.into_policies().map(Policy::from_ast))
}

/// Deserialize the [`PolicySet`] from a JSON string
pub fn from_json_str(src: impl AsRef<str>) -> Result<Self, PolicySetError> {
let est: est::PolicySet = serde_json::from_str(src.as_ref())
Expand Down Expand Up @@ -2273,6 +2368,23 @@ impl std::fmt::Display for PolicySet {
}
}

#[cfg(feature = "protobufs")]
impl Protobuf for PolicySet {
fn encode(&self) -> Vec<u8> {
proto::encode_to_vec::<ast::proto::LiteralPolicySet>(&self.ast)
}
// PANIC SAFETY: experimental feature
#[allow(clippy::expect_used)]
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, prost::DecodeError> {
let ast = proto::try_decode::<ast::proto::LiteralPolicySet, _, _>(buf)?
.expect("proto-encoded policy set should be a valid policy set");
Ok(
PolicySet::from_ast(ast)
.expect("proto-encoded policy set should be a valid policy set"),
)
}
}

/// Given a [`PolicyId`] and a [`Policy`], determine if the policy represents a static policy or a
/// link
fn is_static_or_link(
Expand Down Expand Up @@ -2576,6 +2688,14 @@ impl Template {
})
}

#[cfg_attr(not(feature = "protobufs"), allow(dead_code))]
fn from_ast(ast: ast::Template) -> Self {
Self {
lossless: LosslessPolicy::Est(ast.clone().into()),
ast,
}
}

/// Get the JSON representation of this `Template`.
pub fn to_json(&self) -> Result<serde_json::Value, PolicyToJsonError> {
let est = self.lossless.est()?;
Expand Down Expand Up @@ -2605,6 +2725,16 @@ impl FromStr for Template {
}
}

#[cfg(feature = "protobufs")]
impl Protobuf for Template {
fn encode(&self) -> Vec<u8> {
proto::encode_to_vec::<ast::proto::TemplateBody>(&self.ast)
}
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, prost::DecodeError> {
proto::decode::<ast::proto::TemplateBody, _>(buf).map(Self::from_ast)
}
}

/// Scope constraint on policy principals.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PrincipalConstraint {
Expand Down Expand Up @@ -3078,7 +3208,10 @@ impl Policy {
/// create the `Policy` from the policy text, CST, or EST instead, as the
/// conversion to AST is lossy. ESTs for policies generated by this method
/// will reflect the AST and not the original policy syntax.
#[cfg_attr(not(feature = "partial-eval"), allow(unused))]
#[cfg_attr(
not(any(feature = "partial-eval", feature = "protobufs")),
allow(unused)
)]
pub(crate) fn from_ast(ast: ast::Policy) -> Self {
let text = ast.to_string(); // assume that pretty-printing is faster than `est::Policy::from(ast.clone())`; is that true?
Self {
Expand Down Expand Up @@ -3109,6 +3242,21 @@ impl FromStr for Policy {
}
}

#[cfg(feature = "protobufs")]
impl Protobuf for Policy {
fn encode(&self) -> Vec<u8> {
proto::encode_to_vec::<ast::proto::LiteralPolicy>(&self.ast)
}
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, prost::DecodeError> {
// PANIC SAFETY: experimental feature
#[allow(clippy::expect_used)]
Ok(Self::from_ast(
proto::try_decode::<ast::proto::LiteralPolicy, _, ast::Policy>(buf)?
.expect("protobuf-encoded policy should be a valid policy"),
))
}
}

/// See comments on `Policy` and `Template`.
///
/// This structure can be used for static policies, linked policies, and templates.
Expand Down Expand Up @@ -3278,6 +3426,16 @@ impl FromStr for Expression {
}
}

#[cfg(feature = "protobufs")]
impl Protobuf for Expression {
fn encode(&self) -> Vec<u8> {
proto::encode_to_vec::<ast::proto::Expr>(&self.0)
}
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, prost::DecodeError> {
proto::decode::<ast::proto::Expr, _>(buf).map(Self)
}
}

/// "Restricted" expressions are used for attribute values and `context`.
///
/// Restricted expressions can contain only the following:
Expand Down Expand Up @@ -3601,6 +3759,16 @@ impl Request {
}
}

#[cfg(feature = "protobufs")]
impl Protobuf for Request {
fn encode(&self) -> Vec<u8> {
proto::encode_to_vec::<ast::proto::Request>(&self.0)
}
fn decode(buf: impl prost::bytes::Buf) -> Result<Self, prost::DecodeError> {
proto::decode::<ast::proto::Request, _>(buf).map(Self)
}
}

/// the Context object for an authorization request
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
Expand Down
Loading