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

FFI nits #837

Merged
merged 14 commits into from
Jun 4, 2024
1 change: 1 addition & 0 deletions cedar-policy-core/src/authorizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,7 @@ impl Response {
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
pub enum Decision {
/// The `Authorizer` determined that the request should be allowed
Allow,
Expand Down
2 changes: 1 addition & 1 deletion cedar-policy/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- for the `partial-eval` experimental feature: `PartialResponse` api has changed significantly
- Moved `<PolicyId as FromStr>::Err` to `Infallible` (#588, resolving #551)
- Overhauled the FFI interface in the `frontend` module, and renamed it to
`ffi`; see #757. (#760, #793, #794, #800, #824, more coming)
`ffi`; see #757. (#760, #793, #794, #800, #824, #837, more coming)
- Much richer error information in the FFI interface (#800)
- Deprecated the integration testing harness code. It will be removed from the
`cedar-policy` crate in the next major version.
Expand Down
6 changes: 5 additions & 1 deletion cedar-policy/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ use cedar_policy_validator::RequestValidationError; // this type is unsuitable f
use itertools::{Either, Itertools};
use miette::Diagnostic;
use ref_cast::RefCast;
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::str::FromStr;
Expand Down Expand Up @@ -1060,7 +1061,10 @@ impl From<authorizer::Response> for Response {
}

/// Used to select how a policy will be validated.
#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
#[derive(Default, Eq, PartialEq, Copy, Clone, 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")]
#[non_exhaustive]
Copy link
Contributor

@john-h-kastner-aws john-h-kastner-aws May 3, 2024

Choose a reason for hiding this comment

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

This makes the experimental modes visible in the ffi api where they weren't before. That probably a good thing, but worth thinking about whether that could go wrong.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This shouldn't break anything since the WASM code directly uses the Rust FFI definitions. It should just silently add support for the new validation modes if the cedar_policy crate is compiled with the appropriate flags. What other complications were you thinking of?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think they should be visible in the FFI.

pub enum ValidationMode {
/// Validate that policies do not contain any type errors, and additionally
Expand Down
80 changes: 41 additions & 39 deletions cedar-policy/src/ffi/is_authorized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ use crate::{
Authorizer, Context, Decision, Entities, EntityId, EntityTypeName, EntityUid, PolicyId,
Request, SlotId,
};
use cedar_policy_core::jsonvalue::JsonValueWithNoDuplicateKeys;
use itertools::Itertools;
use miette::{miette, Diagnostic, WrapErr};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -154,10 +153,10 @@ pub struct Response {
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
#[serde(rename_all = "camelCase")]
pub struct Diagnostics {
/// `PolicyId`s of the policies that contributed to the decision.
/// Ids of the policies that contributed to the decision.
/// If no policies applied to the request, this set will be empty.
#[cfg_attr(feature = "wasm", tsify(type = "Set<String>"))]
reason: HashSet<PolicyId>,
#[cfg_attr(feature = "wasm", tsify(type = "Set<String>"))] // REVIEW: still needed?
khieta marked this conversation as resolved.
Show resolved Hide resolved
reason: HashSet<SmolStr>,
/// Set of errors that occurred
errors: HashSet<AuthorizationError>,
}
Expand All @@ -166,7 +165,7 @@ impl Response {
/// Construct a `Response`
pub fn new(
decision: Decision,
reason: HashSet<PolicyId>,
reason: HashSet<SmolStr>,
errors: HashSet<AuthorizationError>,
) -> Self {
Self {
Expand All @@ -191,7 +190,7 @@ impl From<crate::Response> for Response {
let (reason, errors) = response.diagnostics.into_components();
Self::new(
response.decision,
reason.collect(),
reason.map(|id| id.to_smolstr()).collect(),
errors.map(Into::into).collect(),
)
}
Expand All @@ -208,7 +207,7 @@ impl TryFrom<crate::PartialResponse> for Response {

impl Diagnostics {
/// Get the policies that contributed to the decision
pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
pub fn reason(&self) -> impl Iterator<Item = &SmolStr> {
self.reason.iter()
}

Expand Down Expand Up @@ -254,7 +253,7 @@ impl From<crate::AuthorizationError> for AuthorizationError {
fn from(e: crate::AuthorizationError) -> Self {
match e {
crate::AuthorizationError::PolicyEvaluationError { id, error } => {
AuthorizationError::new(id.to_smolstr(), error)
Self::new(id.to_smolstr(), error)
}
}
}
Expand All @@ -276,12 +275,12 @@ impl From<cedar_policy_core::authorizer::AuthorizationError> for AuthorizationEr
#[serde(rename_all = "camelCase")]
pub struct ResidualResponse {
decision: Option<Decision>,
satisfied: HashSet<PolicyId>,
errored: HashSet<PolicyId>,
may_be_determining: HashSet<PolicyId>,
must_be_determining: HashSet<PolicyId>,
residuals: HashMap<PolicyId, serde_json::Value>,
nontrivial_residuals: HashSet<PolicyId>,
satisfied: HashSet<SmolStr>,
errored: HashSet<SmolStr>,
may_be_determining: HashSet<SmolStr>,
must_be_determining: HashSet<SmolStr>,
residuals: HashMap<SmolStr, serde_json::Value>,
nontrivial_residuals: HashSet<SmolStr>,
}

#[cfg(feature = "partial-eval")]
Expand All @@ -292,22 +291,22 @@ impl ResidualResponse {
}

/// Set of all satisfied policy Ids
pub fn satisfied(&self) -> impl Iterator<Item = &PolicyId> {
pub fn satisfied(&self) -> impl Iterator<Item = &SmolStr> {
self.satisfied.iter()
}

/// Set of all policy ids for policies that errored
pub fn errored(&self) -> impl Iterator<Item = &PolicyId> {
pub fn errored(&self) -> impl Iterator<Item = &SmolStr> {
self.errored.iter()
}

/// Over approximation of policies that determine the auth decision
pub fn may_be_determining(&self) -> impl Iterator<Item = &PolicyId> {
pub fn may_be_determining(&self) -> impl Iterator<Item = &SmolStr> {
self.may_be_determining.iter()
}

/// Under approximation of policies that determine the auth decision
pub fn must_be_determining(&self) -> impl Iterator<Item = &PolicyId> {
pub fn must_be_determining(&self) -> impl Iterator<Item = &SmolStr> {
self.must_be_determining.iter()
}

Expand All @@ -321,8 +320,8 @@ impl ResidualResponse {
self.residuals.into_values()
}

/// Get the residual policy for a specified [`PolicyId`] if it exists
pub fn residual(&self, p: &PolicyId) -> Option<&serde_json::Value> {
/// Get the residual policy for a specified id if it exists
pub fn residual(&self, p: &SmolStr) -> Option<&serde_json::Value> {
self.residuals.get(p)
}

Expand All @@ -338,7 +337,7 @@ impl ResidualResponse {
}

/// Iterator over the set of non-trivial residual policy ids
pub fn nontrivial_residual_ids(&self) -> impl Iterator<Item = &PolicyId> {
pub fn nontrivial_residual_ids(&self) -> impl Iterator<Item = &SmolStr> {
self.nontrivial_residuals.iter()
}
}
Expand All @@ -352,24 +351,27 @@ impl TryFrom<crate::PartialResponse> for ResidualResponse {
decision: partial_response.decision(),
satisfied: partial_response
.definitely_satisfied()
.map(|p| p.id().clone())
.map(|p| p.id().to_smolstr())
cdisselkoen marked this conversation as resolved.
Show resolved Hide resolved
.collect(),
errored: partial_response
.definitely_errored()
.map(ToSmolStr::to_smolstr)
.collect(),
errored: partial_response.definitely_errored().cloned().collect(),
may_be_determining: partial_response
.may_be_determining()
.map(|p| p.id().clone())
.map(|p| p.id().to_smolstr())
.collect(),
must_be_determining: partial_response
.must_be_determining()
.map(|p| p.id().clone())
.map(|p| p.id().to_smolstr())
.collect(),
nontrivial_residuals: partial_response
.nontrivial_residuals()
.map(|p| p.id().clone())
.map(|p| p.id().to_smolstr())
.collect(),
residuals: partial_response
.all_residuals()
.map(|e| e.to_json().map(|json| (e.id().clone(), json)))
.map(|e| e.to_json().map(|json| (e.id().to_smolstr(), json)))
.collect::<Result<_, _>>()?,
})
}
Expand Down Expand Up @@ -444,18 +446,18 @@ pub enum PartialAuthorizationAnswer {
pub struct AuthorizationCall {
/// The principal taking action
#[cfg_attr(feature = "wasm", tsify(type = "string|{type: string, id: string}"))]
principal: Option<JsonValueWithNoDuplicateKeys>,
principal: Option<serde_json::Value>,
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure I like this. We went through some effort previously to catch this duplicate key case, and I think that was the right thing to do.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

My thinking was that we want the FFI to use simple types where possible, and users can decide how to handle duplicates when they construct the serde_json::Value.

The other option is to move JsonValueWithNoDuplicateKeys from core to the public API -- would you prefer that?

Copy link
Contributor

Choose a reason for hiding this comment

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

IIRC, we decided downstream users should handle duplicates as they see fit. So in principle I'm fine with moving JsonValueWithNoDuplicateKeys to the public API. I'm not sure this will help anyone using the FFI though because they'll still likely use an equivalent of serde_json::Valueon the other side of the interface.

/// The action the principal is taking
#[cfg_attr(feature = "wasm", tsify(type = "string|{type: string, id: string}"))]
action: JsonValueWithNoDuplicateKeys,
action: serde_json::Value,
/// The resource being acted on by the principal
#[cfg_attr(feature = "wasm", tsify(type = "string|{type: string, id: string}"))]
resource: Option<JsonValueWithNoDuplicateKeys>,
resource: Option<serde_json::Value>,
/// The context details specific to the request
#[serde_as(as = "MapPreventDuplicates<_, _>")]
#[cfg_attr(feature = "wasm", tsify(type = "Record<string, CedarValueJson>"))]
/// The context details specific to the request
context: HashMap<String, JsonValueWithNoDuplicateKeys>,
context: HashMap<String, serde_json::Value>,
/// Optional schema.
/// If present, this will inform the parsing: for instance, it will allow
/// `__entity` and `__extn` escapes to be implicit, and it will error if
Expand All @@ -479,11 +481,11 @@ fn constant_true() -> bool {
/// Parses the given JSON into an [`EntityUid`], or if it fails, adds an
/// appropriate error to `errs` and returns `None`.
fn parse_entity_uid(
entity_uid_json: JsonValueWithNoDuplicateKeys,
entity_uid_json: serde_json::Value,
category: &str,
errs: &mut Vec<miette::Report>,
) -> Option<EntityUid> {
match EntityUid::from_json(entity_uid_json.into())
match EntityUid::from_json(entity_uid_json)
.wrap_err_with(|| format!("Failed to parse {category}"))
{
Ok(euid) => Some(euid),
Expand All @@ -497,7 +499,7 @@ fn parse_entity_uid(
/// Parses the given JSON context into a [`Context`], or if it fails, adds
/// appropriate error(s) to `errs` and returns an empty context.
fn parse_context(
context_map: HashMap<String, JsonValueWithNoDuplicateKeys>,
context_map: HashMap<String, serde_json::Value>,
schema_ref: Option<&crate::Schema>,
action_ref: Option<&EntityUid>,
errs: &mut Vec<miette::Report>,
Expand Down Expand Up @@ -749,7 +751,7 @@ struct RecvdSlice {
/// JSON object containing the entities data, in "natural JSON" form -- same
/// format as expected by EntityJsonParser
#[cfg_attr(feature = "wasm", tsify(type = "Array<EntityJson>"))]
entities: JsonValueWithNoDuplicateKeys,
entities: serde_json::Value,

/// Optional template policies.
#[serde_as(as = "Option<MapPreventDuplicates<_, _>>")]
Expand Down Expand Up @@ -816,7 +818,7 @@ impl RecvdSlice {
crate::PolicySet::new()
}
};
let entities = match Entities::from_json_value(entities.into(), schema) {
let entities = match Entities::from_json_value(entities, schema) {
Ok(entities) => entities,
Err(e) => {
errs.push(miette::Report::new(e));
Expand Down Expand Up @@ -1906,12 +1908,12 @@ mod partial_test {
assert_eq!(response.decision(), None);
let errors: Vec<_> = response.errored().collect();
assert_eq!(errors.len(), 0, "{errors:?}");
let actual_residuals: HashSet<_> = response.nontrivial_residual_ids().collect();
let actual_residuals: HashSet<&str> = response.nontrivial_residual_ids().map(|id| id.as_str()).collect();
for id in &expected_residuals {
assert!(actual_residuals.contains(&PolicyId::from_str(id).ok().unwrap()), "expected nontrivial residual for {id}, but it's missing")
assert!(actual_residuals.contains(id), "expected nontrivial residual for {id}, but it's missing")
}
for id in &actual_residuals {
assert!(expected_residuals.contains(id.to_string().as_str()),"found unexpected nontrivial residual for {id}")
assert!(expected_residuals.contains(id),"found unexpected nontrivial residual for {id}")
khieta marked this conversation as resolved.
Show resolved Hide resolved
}
});
}
Expand Down
13 changes: 4 additions & 9 deletions cedar-policy/src/ffi/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

//! Utility functions and types for JSON interface
use crate::{Policy, SchemaWarning, Template};
use cedar_policy_core::jsonvalue::JsonValueWithNoDuplicateKeys;
use miette::WrapErr;
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, str::FromStr};
Expand Down Expand Up @@ -131,12 +130,10 @@ impl<'a, E: miette::Diagnostic + ?Sized> From<&'a E> for DetailedError {
severity: diag.severity().map(Into::into),
source_locations: diag
.labels()
.map(|labels| labels.map(Into::into).collect())
.unwrap_or_else(|| vec![]),
.map_or_else(Vec::new, |labels| labels.map(Into::into).collect()),
related: diag
.related()
.map(|errs| errs.map(|e| e.into()).collect())
.unwrap_or_else(|| vec![]),
.map_or_else(Vec::new, |errs| errs.map(Into::into).collect()),
}
}
}
Expand Down Expand Up @@ -237,11 +234,9 @@ impl PolicySet {
#[serde(rename_all = "camelCase")]
pub enum Schema {
/// Schema in the Cedar schema format. See <https://docs.cedarpolicy.com/schema/human-readable-schema.html>
#[serde(rename = "human")]
Human(String),
/// Schema in Cedar's JSON schema format. See <https://docs.cedarpolicy.com/schema/json-schema.html>
#[serde(rename = "json")]
Json(JsonValueWithNoDuplicateKeys),
Json(serde_json::Value),
}

impl Schema {
Expand All @@ -257,7 +252,7 @@ impl Schema {
)
})
.map_err(miette::Report::new),
Self::Json(val) => crate::Schema::from_json_value(val.into())
Self::Json(val) => crate::Schema::from_json_value(val)
.map(|sch| {
(
sch,
Expand Down
Loading
Loading