Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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 cedar-policy-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
miette = { version = "7.1.0", features = ["fancy"] }
thiserror = "1.0"
semver = "1.0.23"

[features]
default = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ permit (
principal == User::"alice",
action == Action::"update",
resource == Photo::"VacationPhoto94.jpg"
);
);
13 changes: 12 additions & 1 deletion cedar-policy-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ pub enum Commands {
New(NewArgs),
/// Partially evaluate an authorization request
PartiallyAuthorize(PartiallyAuthorizeArgs),
/// Print Cedar language version
LanguageVersion,
}

#[derive(Args, Debug)]
Expand Down Expand Up @@ -857,7 +859,7 @@ fn format_policies_inner(args: &FormatArgs) -> Result<bool> {
"failed to write formatted policies to {policies_file}"
))?;
}
_ => println!("{}", formatted_policy),
_ => print!("{}", formatted_policy),
}
Ok(are_policies_equivalent)
}
Expand Down Expand Up @@ -1035,6 +1037,15 @@ pub fn new(args: &NewArgs) -> CedarExitCode {
}
}

pub fn language_version() -> CedarExitCode {
let version = get_lang_version();
println!(
"Cedar language version: {}.{}",
version.major, version.minor
);
CedarExitCode::Success
}

fn create_slot_env(data: &HashMap<SlotId, String>) -> Result<HashMap<SlotId, EntityUid>> {
data.iter()
.map(|(key, value)| Ok(EntityUid::from_str(value).map(|euid| (key.clone(), euid))?))
Expand Down
7 changes: 4 additions & 3 deletions cedar-policy-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ use clap::Parser;
use miette::ErrorHook;

use cedar_policy_cli::{
authorize, check_parse, evaluate, format_policies, link, new, partial_authorize,
translate_policy, translate_schema, validate, visualize, CedarExitCode, Cli, Commands,
ErrorFormat,
authorize, check_parse, evaluate, format_policies, language_version, link, new,
partial_authorize, translate_policy, translate_schema, validate, visualize, CedarExitCode, Cli,
Commands, ErrorFormat,
};

fn main() -> CedarExitCode {
Expand Down Expand Up @@ -53,5 +53,6 @@ fn main() -> CedarExitCode {
Commands::TranslateSchema(args) => translate_schema(&args),
Commands::New(args) => new(&args),
Commands::PartiallyAuthorize(args) => partial_authorize(&args),
Commands::LanguageVersion => language_version(),
}
}
3 changes: 2 additions & 1 deletion cedar-policy-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ itertools = "0.13"
ref-cast = "1.0"
rustc_lexer = "0.1"
thiserror = "1.0"
smol_str = { version = "0.2", features = ["serde"] }
smol_str = { version = "0.3", features = ["serde"] }
stacker = "0.1.15"
arbitrary = { version = "1", features = ["derive"], optional = true }
miette = { version = "7.1.0", features = ["serde"] }
Expand Down Expand Up @@ -51,6 +51,7 @@ test-util = []

# Experimental features.
partial-eval = []
entity-tags = []
wasm = ["serde-wasm-bindgen", "tsify", "wasm-bindgen"]

[build-dependencies]
Expand Down
144 changes: 118 additions & 26 deletions cedar-policy-core/src/ast/entity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ pub struct Entity {
uid: EntityUID,

/// Internal BTreMap of attributes.
/// We use a btreemap so that the keys have a determenistic order.
/// We use a btreemap so that the keys have a deterministic order.
///
/// In the serialized form of `Entity`, attribute values appear as
/// `RestrictedExpr`s, for mostly historical reasons.
Expand All @@ -315,6 +315,16 @@ pub struct Entity {
/// Set of ancestors of this `Entity` (i.e., all direct and transitive
/// parents), as UIDs
ancestors: HashSet<EntityUID>,

/// Tags on this entity (RFC 82)
///
/// Like for `attrs`, we use a `BTreeMap` so that the tags have a
/// deterministic order.
/// And like in `attrs`, the values in `tags` appear as `RestrictedExpr` in
/// the serialized form of `Entity`.
#[cfg(feature = "entity-tags")]
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
tags: BTreeMap<SmolStr, PartialValueSerializedAsExpr>,
}

impl std::hash::Hash for Entity {
Expand All @@ -324,54 +334,75 @@ impl std::hash::Hash for Entity {
}

impl Entity {
/// Create a new `Entity` with this UID, attributes, and ancestors
/// Create a new `Entity` with this UID, attributes, ancestors, and tags
///
/// # Errors
/// - Will error if any of the [`RestrictedExpr]`s in `attrs` error when evaluated
/// - Will error if any of the [`RestrictedExpr]`s in `attrs` or `tags` error when evaluated
pub fn new(
uid: EntityUID,
attrs: HashMap<SmolStr, RestrictedExpr>,
attrs: impl IntoIterator<Item = (SmolStr, RestrictedExpr)>,
ancestors: HashSet<EntityUID>,
#[cfg(feature = "entity-tags")] tags: impl IntoIterator<Item = (SmolStr, RestrictedExpr)>,
extensions: &Extensions<'_>,
) -> Result<Self, EntityAttrEvaluationError> {
let evaluator = RestrictedEvaluator::new(extensions);
let evaluate_kvs = |(k, v): (SmolStr, RestrictedExpr), was_attr: bool| {
let attr_val = evaluator
.partial_interpret(v.as_borrowed())
.map_err(|err| EntityAttrEvaluationError {
uid: uid.clone(),
attr_or_tag: k.clone(),
was_attr,
err,
})?;
Ok((k, attr_val.into()))
};
let evaluated_attrs = attrs
.into_iter()
.map(|(k, v)| {
let attr_val = evaluator
.partial_interpret(v.as_borrowed())
.map_err(|err| EntityAttrEvaluationError {
uid: uid.clone(),
attr: k.clone(),
err,
})?;
Ok((k, attr_val.into()))
})
.map(|kv| evaluate_kvs(kv, true))
.collect::<Result<_, EntityAttrEvaluationError>>()?;
#[cfg(feature = "entity-tags")]
let evaluated_tags = tags
.into_iter()
.map(|kv| evaluate_kvs(kv, false))
.collect::<Result<_, EntityAttrEvaluationError>>()?;
Ok(Entity {
uid,
attrs: evaluated_attrs,
ancestors,
#[cfg(feature = "entity-tags")]
tags: evaluated_tags,
})
}

/// Create a new `Entity` with this UID, attributes, and ancestors.
/// Create a new `Entity` with this UID, ancestors, and an empty set of attributes
///
/// Since there are no attributes, this method does not error, and returns `Self` instead of `Result<Self>`
pub fn new_empty_attrs(uid: EntityUID, ancestors: HashSet<EntityUID>) -> Self {
Self::new_with_attr_partial_value(uid, HashMap::new(), ancestors)
}

/// Create a new `Entity` with this UID, attributes, and ancestors (and no tags)
///
/// Unlike in `Entity::new()`, in this constructor, attributes are expressed
/// as `PartialValue`.
///
/// Callers should consider directly using [`Entity::new_with_attr_partial_value_serialized_as_expr`]
/// if they would call this method by first building a map, as it will
/// deconstruct and re-build the map perhaps unnecessarily.
pub fn new_with_attr_partial_value(
uid: EntityUID,
attrs: HashMap<SmolStr, PartialValue>,
attrs: impl IntoIterator<Item = (SmolStr, PartialValue)>,
ancestors: HashSet<EntityUID>,
) -> Self {
Entity {
Self::new_with_attr_partial_value_serialized_as_expr(
uid,
attrs: attrs.into_iter().map(|(k, v)| (k, v.into())).collect(), // TODO(#540): can we do this without disassembling and reassembling the HashMap
attrs.into_iter().map(|(k, v)| (k, v.into())).collect(),
ancestors,
}
)
}

/// Create a new `Entity` with this UID, attributes, and ancestors.
/// Create a new `Entity` with this UID, attributes, and ancestors (and no tags)
///
/// Unlike in `Entity::new()`, in this constructor, attributes are expressed
/// as `PartialValueSerializedAsExpr`.
Expand All @@ -384,6 +415,8 @@ impl Entity {
uid,
attrs,
ancestors,
#[cfg(feature = "entity-tags")]
tags: BTreeMap::new(),
}
}

Expand All @@ -397,6 +430,12 @@ impl Entity {
self.attrs.get(attr).map(|v| v.as_ref())
}

/// Get the value for the given tag, or `None` if not present
#[cfg(feature = "entity-tags")]
pub fn get_tag(&self, tag: &str) -> Option<&PartialValue> {
self.tags.get(tag).map(|v| v.as_ref())
}

/// Is this `Entity` a descendant of `e` in the entity hierarchy?
pub fn is_descendant_of(&self, e: &EntityUID) -> bool {
self.ancestors.contains(e)
Expand All @@ -412,22 +451,42 @@ impl Entity {
self.attrs.len()
}

/// Get the number of tags on this entity
#[cfg(feature = "entity-tags")]
pub fn tags_len(&self) -> usize {
self.tags.len()
}

/// Iterate over this entity's attribute names
pub fn keys(&self) -> impl Iterator<Item = &SmolStr> {
self.attrs.keys()
}

/// Iterate over this entity's tag names
#[cfg(feature = "entity-tags")]
pub fn tag_keys(&self) -> impl Iterator<Item = &SmolStr> {
self.tags.keys()
}

/// Iterate over this entity's attributes
pub fn attrs(&self) -> impl Iterator<Item = (&SmolStr, &PartialValue)> {
self.attrs.iter().map(|(k, v)| (k, v.as_ref()))
}

/// Create an `Entity` with the given UID, no attributes, and no parents.
/// Iterate over this entity's tags
#[cfg(feature = "entity-tags")]
pub fn tags(&self) -> impl Iterator<Item = (&SmolStr, &PartialValue)> {
self.tags.iter().map(|(k, v)| (k, v.as_ref()))
}

/// Create an `Entity` with the given UID, no attributes, no parents, and no tags.
pub fn with_uid(uid: EntityUID) -> Self {
Self {
uid,
attrs: BTreeMap::new(),
ancestors: HashSet::new(),
#[cfg(feature = "entity-tags")]
tags: BTreeMap::new(),
}
}

Expand All @@ -438,6 +497,13 @@ impl Entity {
self.uid == other.uid && self.attrs == other.attrs && self.ancestors == other.ancestors
}

/// Set the UID to the given value.
// Only used for convenience in some tests
#[cfg(test)]
pub fn set_uid(&mut self, uid: EntityUID) {
self.uid = uid;
}

/// Set the given attribute to the given value.
// Only used for convenience in some tests and when fuzzing
#[cfg(any(test, fuzzing))]
Expand All @@ -452,6 +518,21 @@ impl Entity {
Ok(())
}

/// Set the given tag to the given value.
// Only used for convenience in some tests and when fuzzing
#[cfg(any(test, fuzzing))]
#[cfg(feature = "entity-tags")]
pub fn set_tag(
&mut self,
tag: SmolStr,
val: RestrictedExpr,
extensions: &Extensions<'_>,
) -> Result<(), EvaluationError> {
let val = RestrictedEvaluator::new(extensions).partial_interpret(val.as_borrowed())?;
self.tags.insert(tag, val.into());
Ok(())
}

/// Mark the given `UID` as an ancestor of this `Entity`.
// When fuzzing, `add_ancestor()` is fully `pub`.
#[cfg(not(fuzzing))]
Expand All @@ -464,23 +545,30 @@ impl Entity {
self.ancestors.insert(uid);
}

/// Consume the entity and return the entity's owned Uid, attributes and parents.
/// Consume the entity and return the entity's owned Uid, attributes, parents, and tags.
pub fn into_inner(
self,
) -> (
EntityUID,
HashMap<SmolStr, PartialValue>,
HashSet<EntityUID>,
HashMap<SmolStr, PartialValue>,
) {
let Self {
uid,
attrs,
ancestors,
#[cfg(feature = "entity-tags")]
tags,
} = self;
(
uid,
attrs.into_iter().map(|(k, v)| (k, v.0)).collect(),
ancestors,
#[cfg(feature = "entity-tags")]
tags.into_iter().map(|(k, v)| (k, v.0)).collect(),
#[cfg(not(feature = "entity-tags"))]
HashMap::new(),
)
}

Expand Down Expand Up @@ -594,16 +682,20 @@ impl std::fmt::Display for PartialValueSerializedAsExpr {
}
}

/// Error type for evaluation errors when evaluating an entity attribute.
/// Error type for evaluation errors when evaluating an entity attribute or tag.
/// Contains some extra contextual information and the underlying
/// `EvaluationError`.
//
// This is NOT a publicly exported error type.
#[derive(Debug, Diagnostic, Error)]
#[error("failed to evaluate attribute `{attr}` of `{uid}`: {err}")]
#[error("failed to evaluate {} `{attr_or_tag}` of `{uid}`: {err}", if *.was_attr { "attribute" } else { "tag" })]
pub struct EntityAttrEvaluationError {
/// UID of the entity where the error was encountered
pub uid: EntityUID,
/// Attribute of the entity where the error was encountered
pub attr: SmolStr,
/// Attribute or tag of the entity where the error was encountered
pub attr_or_tag: SmolStr,
/// If `attr_or_tag` was an attribute (`true`) or tag (`false`)
pub was_attr: bool,
/// Underlying evaluation error
#[diagnostic(transparent)]
pub err: EvaluationError,
Expand Down
Loading