#![allow(
clippy::missing_panics_doc,
clippy::missing_errors_doc,
clippy::similar_names
)]
pub use ast::Effect;
pub use authorizer::Decision;
#[cfg(feature = "partial-eval")]
use cedar_policy_core::ast::BorrowedRestrictedExpr;
use cedar_policy_core::ast::{self, EntityType};
use cedar_policy_core::ast::{
ContextCreationError, ExprConstructionError, Integer, RestrictedExprParseError,
}; use cedar_policy_core::authorizer;
use cedar_policy_core::entities::{
ContextJsonDeserializationError, ContextSchema, Dereference, JsonDeserializationError,
JsonDeserializationErrorContext,
};
use cedar_policy_core::est;
use cedar_policy_core::evaluator::Evaluator;
#[cfg(feature = "partial-eval")]
use cedar_policy_core::evaluator::RestrictedEvaluator;
pub use cedar_policy_core::evaluator::{EvaluationError, EvaluationErrorKind};
pub use cedar_policy_core::extensions;
use cedar_policy_core::extensions::Extensions;
use cedar_policy_core::parser;
pub use cedar_policy_core::parser::err::ParseErrors;
use cedar_policy_core::FromNormalizedStr;
pub use cedar_policy_validator::human_schema::SchemaWarning;
use cedar_policy_validator::RequestValidationError; pub use cedar_policy_validator::{
TypeErrorKind, UnsupportedFeature, ValidationErrorKind, ValidationWarningKind,
};
use itertools::{Either, Itertools};
use miette::Diagnostic;
use nonempty::NonEmpty;
use ref_cast::RefCast;
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::convert::Infallible;
use std::io::Read;
use std::marker::PhantomData;
use std::str::FromStr;
use thiserror::Error;
pub mod entities {
#[derive(Debug)]
pub struct IntoIter {
pub(super) inner: <cedar_policy_core::entities::Entities as IntoIterator>::IntoIter,
}
impl Iterator for IntoIter {
type Item = super::Entity;
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(super::Entity)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Eq, PartialEq, PartialOrd, Ord, Hash, RefCast)]
pub struct SlotId(ast::SlotId);
impl SlotId {
pub fn principal() -> Self {
Self(ast::SlotId::principal())
}
pub fn resource() -> Self {
Self(ast::SlotId::resource())
}
}
impl std::fmt::Display for SlotId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<ast::SlotId> for SlotId {
fn from(a: ast::SlotId) -> Self {
Self(a)
}
}
impl From<SlotId> for ast::SlotId {
fn from(s: SlotId) -> Self {
s.0
}
}
#[repr(transparent)]
#[derive(Debug, Clone, PartialEq, Eq, RefCast, Hash)]
pub struct Entity(ast::Entity);
impl Entity {
pub fn new(
uid: EntityUid,
attrs: HashMap<String, RestrictedExpression>,
parents: HashSet<EntityUid>,
) -> Result<Self, EntityAttrEvaluationError> {
Ok(Self(ast::Entity::new(
uid.0,
attrs
.into_iter()
.map(|(k, v)| (SmolStr::from(k), v.0))
.collect(),
parents.into_iter().map(|uid| uid.0).collect(),
&Extensions::all_available(),
)?))
}
pub fn new_no_attrs(uid: EntityUid, parents: HashSet<EntityUid>) -> Self {
Self(ast::Entity::new_with_attr_partial_value(
uid.0,
HashMap::new(),
parents.into_iter().map(|uid| uid.0).collect(),
))
}
pub fn with_uid(uid: EntityUid) -> Self {
Self(ast::Entity::with_uid(uid.0))
}
pub fn uid(&self) -> EntityUid {
EntityUid(self.0.uid().clone())
}
pub fn attr(&self, attr: &str) -> Option<Result<EvalResult, impl miette::Diagnostic>> {
let v = match ast::Value::try_from(self.0.get(attr)?.clone()) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
Some(Ok(EvalResult::from(v)))
}
pub fn into_inner(
self,
) -> (
EntityUid,
HashMap<String, RestrictedExpression>,
HashSet<EntityUid>,
) {
let (uid, attrs, ancestors) = self.0.into_inner();
let attrs = attrs
.into_iter()
.map(|(k, v)| {
(
k.to_string(),
match v {
ast::PartialValue::Value(val) => {
RestrictedExpression(ast::RestrictedExpr::from(val))
}
ast::PartialValue::Residual(exp) => {
RestrictedExpression(ast::RestrictedExpr::new_unchecked(exp))
}
},
)
})
.collect();
(
EntityUid(uid),
attrs,
ancestors.into_iter().map(EntityUid).collect(),
)
}
pub fn from_json_value(
value: serde_json::Value,
schema: Option<&Schema>,
) -> Result<Self, EntitiesError> {
let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
let eparser = cedar_policy_core::entities::EntityJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
cedar_policy_core::entities::TCComputation::ComputeNow,
);
eparser.single_from_json_value(value).map(Self)
}
pub fn from_json_str(
src: impl AsRef<str>,
schema: Option<&Schema>,
) -> Result<Self, EntitiesError> {
let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
let eparser = cedar_policy_core::entities::EntityJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
cedar_policy_core::entities::TCComputation::ComputeNow,
);
eparser.single_from_json_str(src).map(Self)
}
pub fn from_json_file(f: impl Read, schema: Option<&Schema>) -> Result<Self, EntitiesError> {
let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
let eparser = cedar_policy_core::entities::EntityJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
cedar_policy_core::entities::TCComputation::ComputeNow,
);
eparser.single_from_json_file(f).map(Self)
}
pub fn write_to_json(&self, f: impl std::io::Write) -> Result<(), EntitiesError> {
self.0.write_to_json(f)
}
pub fn to_json_value(&self) -> Result<serde_json::Value, EntitiesError> {
self.0.to_json_value()
}
pub fn to_json_string(&self) -> Result<String, EntitiesError> {
self.0.to_json_string()
}
}
impl std::fmt::Display for Entity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Default, PartialEq, Eq, RefCast)]
pub struct Entities(pub(crate) cedar_policy_core::entities::Entities);
pub use cedar_policy_core::entities::EntitiesError;
impl Entities {
pub fn empty() -> Self {
Self(cedar_policy_core::entities::Entities::new())
}
pub fn get(&self, uid: &EntityUid) -> Option<&Entity> {
match self.0.entity(&uid.0) {
Dereference::Residual(_) | Dereference::NoSuchEntity => None,
Dereference::Data(e) => Some(Entity::ref_cast(e)),
}
}
#[doc = include_str!("../experimental_warning.md")]
#[must_use]
#[cfg(feature = "partial-eval")]
pub fn partial(self) -> Self {
Self(self.0.partial())
}
pub fn iter(&self) -> impl Iterator<Item = &Entity> {
self.0.iter().map(Entity::ref_cast)
}
pub fn from_entities(
entities: impl IntoIterator<Item = Entity>,
schema: Option<&Schema>,
) -> Result<Self, cedar_policy_core::entities::EntitiesError> {
cedar_policy_core::entities::Entities::from_entities(
entities.into_iter().map(|e| e.0),
schema
.map(|s| cedar_policy_validator::CoreSchema::new(&s.0))
.as_ref(),
cedar_policy_core::entities::TCComputation::ComputeNow,
Extensions::all_available(),
)
.map(Entities)
}
pub fn add_entities(
self,
entities: impl IntoIterator<Item = Entity>,
schema: Option<&Schema>,
) -> Result<Self, EntitiesError> {
Ok(Self(
self.0.add_entities(
entities.into_iter().map(|e| e.0),
schema
.map(|s| cedar_policy_validator::CoreSchema::new(&s.0))
.as_ref(),
cedar_policy_core::entities::TCComputation::ComputeNow,
Extensions::all_available(),
)?,
))
}
pub fn add_entities_from_json_str(
self,
json: &str,
schema: Option<&Schema>,
) -> Result<Self, EntitiesError> {
let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
let eparser = cedar_policy_core::entities::EntityJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
cedar_policy_core::entities::TCComputation::ComputeNow,
);
let new_entities = eparser.iter_from_json_str(json)?;
Ok(Self(self.0.add_entities(
new_entities,
schema.as_ref(),
cedar_policy_core::entities::TCComputation::ComputeNow,
Extensions::all_available(),
)?))
}
pub fn add_entities_from_json_value(
self,
json: serde_json::Value,
schema: Option<&Schema>,
) -> Result<Self, EntitiesError> {
let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
let eparser = cedar_policy_core::entities::EntityJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
cedar_policy_core::entities::TCComputation::ComputeNow,
);
let new_entities = eparser.iter_from_json_value(json)?;
Ok(Self(self.0.add_entities(
new_entities,
schema.as_ref(),
cedar_policy_core::entities::TCComputation::ComputeNow,
Extensions::all_available(),
)?))
}
pub fn add_entities_from_json_file(
self,
json: impl std::io::Read,
schema: Option<&Schema>,
) -> Result<Self, EntitiesError> {
let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
let eparser = cedar_policy_core::entities::EntityJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
cedar_policy_core::entities::TCComputation::ComputeNow,
);
let new_entities = eparser.iter_from_json_file(json)?;
Ok(Self(self.0.add_entities(
new_entities,
schema.as_ref(),
cedar_policy_core::entities::TCComputation::ComputeNow,
Extensions::all_available(),
)?))
}
pub fn from_json_str(
json: &str,
schema: Option<&Schema>,
) -> Result<Self, cedar_policy_core::entities::EntitiesError> {
let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
let eparser = cedar_policy_core::entities::EntityJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
cedar_policy_core::entities::TCComputation::ComputeNow,
);
eparser.from_json_str(json).map(Entities)
}
pub fn from_json_value(
json: serde_json::Value,
schema: Option<&Schema>,
) -> Result<Self, cedar_policy_core::entities::EntitiesError> {
let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
let eparser = cedar_policy_core::entities::EntityJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
cedar_policy_core::entities::TCComputation::ComputeNow,
);
eparser.from_json_value(json).map(Entities)
}
pub fn from_json_file(
json: impl std::io::Read,
schema: Option<&Schema>,
) -> Result<Self, cedar_policy_core::entities::EntitiesError> {
let schema = schema.map(|s| cedar_policy_validator::CoreSchema::new(&s.0));
let eparser = cedar_policy_core::entities::EntityJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
cedar_policy_core::entities::TCComputation::ComputeNow,
);
eparser.from_json_file(json).map(Entities)
}
pub fn is_ancestor_of(&self, a: &EntityUid, b: &EntityUid) -> bool {
match self.0.entity(&b.0) {
Dereference::Data(b) => b.is_descendant_of(&a.0),
_ => a == b, }
}
pub fn ancestors<'a>(
&'a self,
euid: &EntityUid,
) -> Option<impl Iterator<Item = &'a EntityUid>> {
let entity = match self.0.entity(&euid.0) {
Dereference::Residual(_) | Dereference::NoSuchEntity => None,
Dereference::Data(e) => Some(e),
}?;
Some(entity.ancestors().map(EntityUid::ref_cast))
}
pub fn write_to_json(
&self,
f: impl std::io::Write,
) -> std::result::Result<(), cedar_policy_core::entities::EntitiesError> {
self.0.write_to_json(f)
}
#[doc = include_str!("../experimental_warning.md")]
pub fn to_dot_str(&self) -> String {
self.0.to_dot_str()
}
}
impl IntoIterator for Entities {
type Item = Entity;
type IntoIter = entities::IntoIter;
fn into_iter(self) -> Self::IntoIter {
Self::IntoIter {
inner: self.0.into_iter(),
}
}
}
#[repr(transparent)]
#[derive(Debug, RefCast)]
pub struct Authorizer(authorizer::Authorizer);
impl Default for Authorizer {
fn default() -> Self {
Self::new()
}
}
impl Authorizer {
pub fn new() -> Self {
Self(authorizer::Authorizer::new())
}
pub fn is_authorized(&self, r: &Request, p: &PolicySet, e: &Entities) -> Response {
self.0.is_authorized(r.0.clone(), &p.ast, &e.0).into()
}
#[doc = include_str!("../experimental_warning.md")]
#[cfg(feature = "partial-eval")]
pub fn is_authorized_partial(
&self,
query: &Request,
policy_set: &PolicySet,
entities: &Entities,
) -> PartialResponse {
let response = self
.0
.is_authorized_core(query.0.clone(), &policy_set.ast, &entities.0);
PartialResponse(response)
}
}
#[derive(Debug, Diagnostic, PartialEq, Eq, Error, Clone)]
pub enum AuthorizationError {
#[error("while evaluating policy `{id}`: {error}")]
PolicyEvaluationError {
#[doc(hidden)]
id: ast::PolicyID,
#[diagnostic(transparent)]
error: EvaluationError,
},
}
impl AuthorizationError {
pub fn id(&self) -> &PolicyId {
match self {
Self::PolicyEvaluationError { id, error: _ } => PolicyId::ref_cast(id),
}
}
}
#[doc(hidden)]
impl From<authorizer::AuthorizationError> for AuthorizationError {
fn from(value: authorizer::AuthorizationError) -> Self {
match value {
authorizer::AuthorizationError::PolicyEvaluationError { id, error } => {
Self::PolicyEvaluationError { id, error }
}
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Response {
pub(crate) decision: Decision,
pub(crate) diagnostics: Diagnostics,
}
#[doc = include_str!("../experimental_warning.md")]
#[cfg(feature = "partial-eval")]
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Clone, RefCast)]
pub struct PartialResponse(cedar_policy_core::authorizer::PartialResponse);
#[cfg(feature = "partial-eval")]
impl PartialResponse {
pub fn decision(&self) -> Option<Decision> {
self.0.decision()
}
pub fn concretize(self) -> Response {
self.0.concretize().into()
}
pub fn definitely_satisfied(&self) -> impl Iterator<Item = Policy> + '_ {
self.0.definitely_satisfied().map(Policy::from_ast)
}
pub fn definitely_errored(&self) -> impl Iterator<Item = &PolicyId> {
self.0.definitely_errored().map(PolicyId::ref_cast)
}
pub fn may_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
self.0.may_be_determining().map(Policy::from_ast)
}
pub fn must_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
self.0.must_be_determining().map(Policy::from_ast)
}
pub fn nontrivial_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
self.0.nontrivial_residuals().map(Policy::from_ast)
}
pub fn all_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
self.0.all_residuals().map(Policy::from_ast)
}
pub fn get(&self, id: &PolicyId) -> Option<Policy> {
self.0.get(&id.0).map(Policy::from_ast)
}
pub fn reauthorize(
&self,
mapping: HashMap<SmolStr, RestrictedExpression>,
auth: &Authorizer,
r: Request,
es: &Entities,
) -> Result<Self, ReAuthorizeError> {
let exts = Extensions::all_available();
let evaluator = RestrictedEvaluator::new(&exts);
let mapping = mapping
.into_iter()
.map(|(name, expr)| {
evaluator
.interpret(BorrowedRestrictedExpr::new_unchecked(expr.0.as_ref()))
.map(|v| (name, v))
})
.collect::<Result<HashMap<_, _>, EvaluationError>>()?;
let r = self.0.reauthorize(&mapping, &auth.0, r.0, &es.0)?;
Ok(Self(r))
}
}
#[cfg(feature = "partial-eval")]
#[doc(hidden)]
impl From<cedar_policy_core::authorizer::PartialResponse> for PartialResponse {
fn from(pr: cedar_policy_core::authorizer::PartialResponse) -> Self {
Self(pr)
}
}
#[derive(Debug, Error)]
pub enum ReAuthorizeError {
#[error("{err}")]
Evaluation {
#[from]
err: EvaluationError,
},
#[error("{err}")]
PolicySet {
#[from]
err: cedar_policy_core::ast::PolicySetError,
},
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Diagnostics {
reason: HashSet<PolicyId>,
errors: Vec<AuthorizationError>,
}
impl From<authorizer::Diagnostics> for Diagnostics {
fn from(diagnostics: authorizer::Diagnostics) -> Self {
Self {
reason: diagnostics.reason.into_iter().map(PolicyId).collect(),
errors: diagnostics.errors.into_iter().map(Into::into).collect(),
}
}
}
impl Diagnostics {
pub fn reason(&self) -> impl Iterator<Item = &PolicyId> {
self.reason.iter()
}
pub fn errors(&self) -> impl Iterator<Item = &AuthorizationError> + '_ {
self.errors.iter()
}
pub(crate) fn into_components(
self,
) -> (
impl Iterator<Item = PolicyId>,
impl Iterator<Item = AuthorizationError>,
) {
(self.reason.into_iter(), self.errors.into_iter())
}
}
impl Response {
pub fn new(
decision: Decision,
reason: HashSet<PolicyId>,
errors: Vec<AuthorizationError>,
) -> Self {
Self {
decision,
diagnostics: Diagnostics { reason, errors },
}
}
pub fn decision(&self) -> Decision {
self.decision
}
pub fn diagnostics(&self) -> &Diagnostics {
&self.diagnostics
}
}
impl From<authorizer::Response> for Response {
fn from(a: authorizer::Response) -> Self {
Self {
decision: a.decision,
diagnostics: a.diagnostics.into(),
}
}
}
#[derive(Default, Eq, PartialEq, Copy, Clone, Debug)]
#[non_exhaustive]
pub enum ValidationMode {
#[default]
Strict,
#[doc = include_str!("../experimental_warning.md")]
#[cfg(feature = "permissive-validate")]
Permissive,
#[doc = include_str!("../experimental_warning.md")]
#[cfg(feature = "partial-validate")]
Partial,
}
impl From<ValidationMode> for cedar_policy_validator::ValidationMode {
fn from(mode: ValidationMode) -> Self {
match mode {
ValidationMode::Strict => Self::Strict,
#[cfg(feature = "permissive-validate")]
ValidationMode::Permissive => Self::Permissive,
#[cfg(feature = "partial-validate")]
ValidationMode::Partial => Self::Partial,
}
}
}
#[repr(transparent)]
#[derive(Debug, RefCast)]
pub struct Validator(cedar_policy_validator::Validator);
impl Validator {
pub fn new(schema: Schema) -> Self {
Self(cedar_policy_validator::Validator::new(schema.0))
}
pub fn validate<'a>(
&'a self,
pset: &'a PolicySet,
mode: ValidationMode,
) -> ValidationResult<'static> {
ValidationResult::from(self.0.validate(&pset.ast, mode.into()))
}
}
#[derive(Debug)]
pub struct SchemaFragment {
value: cedar_policy_validator::ValidatorSchemaFragment,
lossless: cedar_policy_validator::SchemaFragment,
}
impl SchemaFragment {
pub fn namespaces(&self) -> impl Iterator<Item = Option<EntityNamespace>> + '_ {
self.value
.namespaces()
.map(|ns| ns.as_ref().map(|ns| EntityNamespace(ns.clone())))
}
pub fn from_json_str(src: &str) -> Result<Self, SchemaError> {
let lossless = cedar_policy_validator::SchemaFragment::from_json_str(src)?;
Ok(Self {
value: lossless.clone().try_into()?,
lossless,
})
}
pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
let lossless = cedar_policy_validator::SchemaFragment::from_json_value(json)?;
Ok(Self {
value: lossless.clone().try_into()?,
lossless,
})
}
#[deprecated(since = "3.3.0", note = "Use `from_cedarschema_file()` instead")]
pub fn from_file_natural(
r: impl std::io::Read,
) -> Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
let (lossless, warnings) = cedar_policy_validator::SchemaFragment::from_file_natural(r)?;
Ok((
Self {
value: lossless.clone().try_into()?,
lossless,
},
warnings,
))
}
#[allow(deprecated)]
pub fn from_cedarschema_file(
r: impl std::io::Read,
) -> Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
Self::from_file_natural(r)
}
#[deprecated(since = "3.3.0", note = "Use `from_cedarschema_str()` instead")]
pub fn from_str_natural(
src: &str,
) -> Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
let (lossless, warnings) = cedar_policy_validator::SchemaFragment::from_str_natural(src)?;
Ok((
Self {
value: lossless.clone().try_into()?,
lossless,
},
warnings,
))
}
#[allow(deprecated)]
pub fn from_cedarschema_str(
src: &str,
) -> Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
Self::from_str_natural(src)
}
#[deprecated(since = "3.3.0", note = "Use `from_json_file()` instead")]
pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
let lossless = cedar_policy_validator::SchemaFragment::from_file(file)?;
Ok(Self {
value: lossless.clone().try_into()?,
lossless,
})
}
#[allow(deprecated)]
pub fn from_json_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
Self::from_file(file)
}
pub fn to_json_value(self) -> Result<serde_json::Value, SchemaError> {
let v = serde_json::to_value(self.lossless)?;
Ok(v)
}
#[deprecated(since = "3.3.0", note = "Use `to_json_string()` instead")]
pub fn as_json_string(&self) -> Result<String, SchemaError> {
let str = serde_json::to_string(&self.lossless)?;
Ok(str)
}
#[allow(deprecated)]
pub fn to_json_string(&self) -> Result<String, SchemaError> {
self.as_json_string()
}
#[deprecated(since = "3.3.0", note = "Use `to_cedarschema()` instead")]
pub fn as_natural(&self) -> Result<String, ToHumanSyntaxError> {
let str = self.lossless.as_natural_schema()?;
Ok(str)
}
#[allow(deprecated)]
pub fn to_cedarschema(&self) -> Result<String, ToHumanSyntaxError> {
self.as_natural()
}
}
impl TryInto<Schema> for SchemaFragment {
type Error = SchemaError;
fn try_into(self) -> Result<Schema, Self::Error> {
Ok(Schema(
cedar_policy_validator::ValidatorSchema::from_schema_fragments([self.value])?,
))
}
}
impl FromStr for SchemaFragment {
type Err = SchemaError;
fn from_str(src: &str) -> Result<Self, Self::Err> {
let lossless = serde_json::from_str::<cedar_policy_validator::SchemaFragment>(src)?;
Ok(Self {
value: lossless.clone().try_into()?,
lossless,
})
}
}
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
pub struct Schema(pub(crate) cedar_policy_validator::ValidatorSchema);
impl FromStr for Schema {
type Err = SchemaError;
fn from_str(schema_src: &str) -> Result<Self, Self::Err> {
Ok(Self(schema_src.parse()?))
}
}
impl Schema {
pub fn from_schema_fragments(
fragments: impl IntoIterator<Item = SchemaFragment>,
) -> Result<Self, SchemaError> {
Ok(Self(
cedar_policy_validator::ValidatorSchema::from_schema_fragments(
fragments.into_iter().map(|f| f.value),
)?,
))
}
pub fn from_json_value(json: serde_json::Value) -> Result<Self, SchemaError> {
Ok(Self(
cedar_policy_validator::ValidatorSchema::from_json_value(
json,
Extensions::all_available(),
)?,
))
}
#[deprecated(since = "3.3.0", note = "Use `from_json_file()` instead")]
pub fn from_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
Ok(Self(cedar_policy_validator::ValidatorSchema::from_file(
file,
Extensions::all_available(),
)?))
}
#[allow(deprecated)]
pub fn from_json_file(file: impl std::io::Read) -> Result<Self, SchemaError> {
Self::from_file(file)
}
#[deprecated(since = "3.3.0", note = "Use `from_cedarschema_file()` instead")]
pub fn from_file_natural(
file: impl std::io::Read,
) -> Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
let (schema, warnings) = cedar_policy_validator::ValidatorSchema::from_file_natural(
file,
Extensions::all_available(),
)?;
Ok((Self(schema), warnings))
}
#[allow(deprecated)]
pub fn from_cedarschema_file(
file: impl std::io::Read,
) -> Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
Self::from_file_natural(file)
}
#[deprecated(since = "3.3.0", note = "Use `from_cedarschema_str()` instead")]
pub fn from_str_natural(
src: &str,
) -> Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
let (schema, warnings) = cedar_policy_validator::ValidatorSchema::from_str_natural(
src,
Extensions::all_available(),
)?;
Ok((Self(schema), warnings))
}
#[allow(deprecated)]
pub fn from_cedarschema_str(
src: &str,
) -> Result<(Self, impl Iterator<Item = SchemaWarning>), HumanSchemaError> {
Self::from_str_natural(src)
}
pub fn action_entities(&self) -> Result<Entities, EntitiesError> {
Ok(Entities(self.0.action_entities()?))
}
pub fn principals(&self) -> impl Iterator<Item = &EntityTypeName> {
self.0.principals().filter_map(|ty| match ty {
EntityType::Specified(name) => Some(EntityTypeName::ref_cast(name)),
EntityType::Unspecified => None,
})
}
pub fn resources(&self) -> impl Iterator<Item = &EntityTypeName> {
self.0.resources().filter_map(|ty| match ty {
EntityType::Specified(name) => Some(EntityTypeName::ref_cast(name)),
EntityType::Unspecified => None,
})
}
pub fn principals_for_action(
&self,
action: &EntityUid,
) -> Option<impl Iterator<Item = &EntityTypeName>> {
self.0.principals_for_action(&action.0).map(|iter| {
iter.filter_map(|ty| match ty {
EntityType::Specified(name) => Some(EntityTypeName::ref_cast(name)),
EntityType::Unspecified => None,
})
})
}
pub fn resources_for_action(
&self,
action: &EntityUid,
) -> Option<impl Iterator<Item = &EntityTypeName>> {
self.0.resources_for_action(&action.0).map(|iter| {
iter.filter_map(|ty| match ty {
EntityType::Specified(name) => Some(EntityTypeName::ref_cast(name)),
EntityType::Unspecified => None,
})
})
}
pub fn ancestors<'a>(
&'a self,
ty: &'a EntityTypeName,
) -> Option<impl Iterator<Item = &'a EntityTypeName> + 'a> {
self.0
.ancestors(&ty.0)
.map(|iter| iter.map(RefCast::ref_cast))
}
pub fn action_groups(&self) -> impl Iterator<Item = &EntityUid> {
self.0.action_groups().map(RefCast::ref_cast)
}
pub fn entity_types(&self) -> impl Iterator<Item = &EntityTypeName> {
self.0
.entity_types()
.map(|(name, _)| RefCast::ref_cast(name))
}
pub fn actions(&self) -> impl Iterator<Item = &EntityUid> {
self.0.actions().map(RefCast::ref_cast)
}
}
#[derive(Debug, Diagnostic, Error)]
pub enum SchemaError {
#[error("failed to parse schema: {0}")]
Serde(#[from] serde_json::Error),
#[error("transitive closure computation/enforcement error on action hierarchy: {0}")]
ActionTransitiveClosure(String),
#[error("transitive closure computation/enforcement error on entity type hierarchy: {0}")]
EntityTypeTransitiveClosure(String),
#[error("unsupported feature used in schema: {0}")]
UnsupportedFeature(String),
#[error("undeclared entity type(s): {0:?}")]
UndeclaredEntityTypes(HashSet<String>),
#[error("undeclared action(s): {0:?}")]
UndeclaredActions(HashSet<String>),
#[error("undeclared common type(s): {0:?}")]
UndeclaredCommonTypes(HashSet<String>),
#[error("duplicate entity type `{0}`")]
DuplicateEntityType(String),
#[error("duplicate action `{0}`")]
DuplicateAction(String),
#[error("duplicate common type `{0}`")]
DuplicateCommonType(String),
#[error("cycle in action hierarchy containing `{0}`")]
CycleInActionHierarchy(EntityUid),
#[error("parse error in entity type: {0}")]
#[diagnostic(transparent)]
#[deprecated(
since = "3.2.0",
note = "Entity type parse errors are now detected during JSON parsing and reported as `SchemaError::Serde`"
)]
ParseEntityType(ParseErrors),
#[error("parse error in namespace identifier: {0}")]
#[diagnostic(transparent)]
#[deprecated(
since = "3.2.0",
note = "Namespace parse errors are now detected during JSON parsing and reported as `SchemaError::Serde`"
)]
ParseNamespace(ParseErrors),
#[error("parse error in extension type: {0}")]
#[diagnostic(transparent)]
#[deprecated(
since = "3.2.0",
note = "Extension type parse errors are now detected during JSON parsing and reported as `SchemaError::Serde`"
)]
ParseExtensionType(ParseErrors),
#[error("parse error in common type identifier: {0}")]
#[diagnostic(transparent)]
#[deprecated(
since = "3.2.0",
note = "Common type parse errors are now detected during JSON parsing and reported as `SchemaError::Serde`"
)]
ParseCommonType(ParseErrors),
#[error("entity type `Action` declared in `entityTypes` list")]
ActionEntityTypeDeclared,
#[error("{0} is declared with a type other than `Record`")]
ContextOrShapeNotRecord(ContextOrShape),
#[error("action `{0}` has an attribute that is an empty set")]
ActionAttributesContainEmptySet(EntityUid),
#[error("action `{0}` has an attribute with unsupported JSON representation: {1}")]
UnsupportedActionAttribute(EntityUid, String),
#[error(transparent)]
#[diagnostic(transparent)]
ActionAttrEval(EntityAttrEvaluationError),
#[error("schema contained the non-supported `__expr` escape")]
ExprEscapeUsed,
}
#[derive(Debug, Error, Diagnostic)]
pub enum ToHumanSyntaxError {
#[error("There are type name collisions: [{}]", .0.iter().join(", "))]
NameCollisions(NonEmpty<SmolStr>),
}
impl From<cedar_policy_validator::human_schema::ToHumanSchemaStrError> for ToHumanSyntaxError {
fn from(value: cedar_policy_validator::human_schema::ToHumanSchemaStrError) -> Self {
match value {
cedar_policy_validator::human_schema::ToHumanSchemaStrError::NameCollisions(
collisions,
) => Self::NameCollisions(collisions),
}
}
}
#[derive(Debug, Diagnostic, Error)]
pub enum HumanSchemaError {
#[error("Error parsing schema: {0}")]
#[diagnostic(transparent)]
ParseError(#[from] cedar_policy_validator::human_schema::parser::HumanSyntaxParseErrors),
#[error("{0}")]
#[diagnostic(transparent)]
Core(#[from] SchemaError),
#[error("{0}")]
Io(#[from] std::io::Error),
}
#[doc(hidden)]
impl From<cedar_policy_validator::HumanSchemaError> for HumanSchemaError {
fn from(value: cedar_policy_validator::HumanSchemaError) -> Self {
match value {
cedar_policy_validator::HumanSchemaError::Core(core) => Self::Core(core.into()),
cedar_policy_validator::HumanSchemaError::IO(io_err) => Self::Io(io_err),
cedar_policy_validator::HumanSchemaError::Parsing(e) => Self::ParseError(e),
}
}
}
impl From<cedar_policy_validator::SchemaError> for HumanSchemaError {
fn from(value: cedar_policy_validator::SchemaError) -> Self {
Self::Core(value.into())
}
}
#[derive(Debug, Diagnostic, Error)]
#[error("in attribute `{attr}` of `{uid}`: {err}")]
pub struct EntityAttrEvaluationError {
pub uid: EntityUid,
pub attr: SmolStr,
#[diagnostic(transparent)]
pub err: EvaluationError,
}
impl From<ast::EntityAttrEvaluationError> for EntityAttrEvaluationError {
fn from(err: ast::EntityAttrEvaluationError) -> Self {
Self {
uid: EntityUid(err.uid),
attr: err.attr,
err: err.err,
}
}
}
#[derive(Debug)]
pub enum ContextOrShape {
ActionContext(EntityUid),
EntityTypeShape(EntityTypeName),
}
impl std::fmt::Display for ContextOrShape {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ActionContext(action) => write!(f, "Context for action {action}"),
Self::EntityTypeShape(entity_type) => {
write!(f, "Shape for entity type {entity_type}")
}
}
}
}
impl From<cedar_policy_validator::ContextOrShape> for ContextOrShape {
fn from(value: cedar_policy_validator::ContextOrShape) -> Self {
match value {
cedar_policy_validator::ContextOrShape::ActionContext(euid) => {
Self::ActionContext(EntityUid(euid))
}
cedar_policy_validator::ContextOrShape::EntityTypeShape(name) => {
Self::EntityTypeShape(EntityTypeName(name))
}
}
}
}
#[doc(hidden)]
impl From<cedar_policy_validator::SchemaError> for SchemaError {
fn from(value: cedar_policy_validator::SchemaError) -> Self {
match value {
cedar_policy_validator::SchemaError::Serde(e) => Self::Serde(e),
cedar_policy_validator::SchemaError::ActionTransitiveClosure(e) => {
Self::ActionTransitiveClosure(e.to_string())
}
cedar_policy_validator::SchemaError::EntityTypeTransitiveClosure(e) => {
Self::EntityTypeTransitiveClosure(e.to_string())
}
cedar_policy_validator::SchemaError::UnsupportedFeature(e) => {
Self::UnsupportedFeature(e.to_string())
}
cedar_policy_validator::SchemaError::UndeclaredEntityTypes(e) => {
Self::UndeclaredEntityTypes(e)
}
cedar_policy_validator::SchemaError::UndeclaredActions(e) => Self::UndeclaredActions(e),
cedar_policy_validator::SchemaError::UndeclaredCommonTypes(c) => {
Self::UndeclaredCommonTypes(c)
}
cedar_policy_validator::SchemaError::DuplicateEntityType(e) => {
Self::DuplicateEntityType(e)
}
cedar_policy_validator::SchemaError::DuplicateAction(e) => Self::DuplicateAction(e),
cedar_policy_validator::SchemaError::DuplicateCommonType(c) => {
Self::DuplicateCommonType(c)
}
cedar_policy_validator::SchemaError::CycleInActionHierarchy(e) => {
Self::CycleInActionHierarchy(EntityUid(e))
}
cedar_policy_validator::SchemaError::CycleInCommonTypeReferences(_) => {
Self::Serde(serde::de::Error::custom(value))
}
cedar_policy_validator::SchemaError::ActionEntityTypeDeclared => {
Self::ActionEntityTypeDeclared
}
cedar_policy_validator::SchemaError::ContextOrShapeNotRecord(context_or_shape) => {
Self::ContextOrShapeNotRecord(context_or_shape.into())
}
cedar_policy_validator::SchemaError::ActionAttributesContainEmptySet(uid) => {
Self::ActionAttributesContainEmptySet(EntityUid(uid))
}
cedar_policy_validator::SchemaError::UnsupportedActionAttribute(uid, escape_type) => {
Self::UnsupportedActionAttribute(EntityUid(uid), escape_type)
}
cedar_policy_validator::SchemaError::ActionAttrEval(err) => {
Self::ActionAttrEval(err.into())
}
cedar_policy_validator::SchemaError::ExprEscapeUsed => Self::ExprEscapeUsed,
}
}
}
#[derive(Debug)]
pub struct ValidationResult<'a> {
validation_errors: Vec<ValidationError<'static>>,
validation_warnings: Vec<ValidationWarning<'static>>,
phantom: PhantomData<&'a ()>,
}
impl<'a> ValidationResult<'a> {
pub fn validation_passed(&self) -> bool {
self.validation_errors.is_empty()
}
pub fn validation_passed_without_warnings(&self) -> bool {
self.validation_errors.is_empty() && self.validation_warnings.is_empty()
}
pub fn validation_errors(&self) -> impl Iterator<Item = &ValidationError<'static>> {
self.validation_errors.iter()
}
pub fn validation_warnings(&self) -> impl Iterator<Item = &ValidationWarning<'static>> {
self.validation_warnings.iter()
}
fn first_error_or_warning(&self) -> Option<&dyn Diagnostic> {
self.validation_errors
.first()
.map(|e| e as &dyn Diagnostic)
.or_else(|| {
self.validation_warnings
.first()
.map(|w| w as &dyn Diagnostic)
})
}
pub(crate) fn into_errors_and_warnings(
self,
) -> (
impl Iterator<Item = ValidationError<'static>>,
impl Iterator<Item = ValidationWarning<'static>>,
) {
(
self.validation_errors.into_iter(),
self.validation_warnings.into_iter(),
)
}
}
impl<'a> From<cedar_policy_validator::ValidationResult<'a>> for ValidationResult<'static> {
fn from(r: cedar_policy_validator::ValidationResult<'a>) -> Self {
let (errors, warnings) = r.into_errors_and_warnings();
Self {
validation_errors: errors.map(ValidationError::from).collect(),
validation_warnings: warnings.map(ValidationWarning::from).collect(),
phantom: PhantomData,
}
}
}
impl<'a> std::fmt::Display for ValidationResult<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.first_error_or_warning() {
Some(diagnostic) => write!(f, "{diagnostic}"),
None => write!(f, "no errors or warnings"),
}
}
}
impl<'a> std::error::Error for ValidationResult<'a> {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.first_error_or_warning()
.and_then(std::error::Error::source)
}
#[allow(deprecated)]
fn description(&self) -> &str {
self.first_error_or_warning()
.map_or("no errors or warnings", std::error::Error::description)
}
#[allow(deprecated)]
fn cause(&self) -> Option<&dyn std::error::Error> {
self.first_error_or_warning()
.and_then(std::error::Error::cause)
}
}
impl<'a> Diagnostic for ValidationResult<'a> {
fn related<'s>(&'s self) -> Option<Box<dyn Iterator<Item = &'s dyn Diagnostic> + 's>> {
let mut related = self
.validation_errors
.iter()
.map(|err| err as &dyn Diagnostic)
.chain(
self.validation_warnings
.iter()
.map(|warn| warn as &dyn Diagnostic),
);
related.next().map(move |first| match first.related() {
Some(first_related) => Box::new(first_related.chain(related)),
None => Box::new(related) as Box<dyn Iterator<Item = _>>,
})
}
fn severity(&self) -> Option<miette::Severity> {
self.first_error_or_warning()
.map_or(Some(miette::Severity::Advice), Diagnostic::severity)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
self.first_error_or_warning().and_then(Diagnostic::labels)
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.first_error_or_warning()
.and_then(Diagnostic::source_code)
}
fn code<'s>(&'s self) -> Option<Box<dyn std::fmt::Display + 's>> {
self.first_error_or_warning().and_then(Diagnostic::code)
}
fn url<'s>(&'s self) -> Option<Box<dyn std::fmt::Display + 's>> {
self.first_error_or_warning().and_then(Diagnostic::url)
}
fn help<'s>(&'s self) -> Option<Box<dyn std::fmt::Display + 's>> {
self.first_error_or_warning().and_then(Diagnostic::help)
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.first_error_or_warning()
.and_then(Diagnostic::diagnostic_source)
}
}
#[derive(Debug, Clone, Error)]
#[error("validation error on {location}: {}", self.error_kind())]
pub struct ValidationError<'a> {
location: SourceLocation<'static>,
error_kind: ValidationErrorKind,
phantom: PhantomData<&'a ()>,
}
impl<'a> ValidationError<'a> {
pub fn error_kind(&self) -> &ValidationErrorKind {
&self.error_kind
}
pub fn location(&self) -> &SourceLocation<'a> {
&self.location
}
}
#[doc(hidden)]
impl<'a> From<cedar_policy_validator::ValidationError<'a>> for ValidationError<'static> {
fn from(err: cedar_policy_validator::ValidationError<'a>) -> Self {
let (location, error_kind) = err.into_location_and_error_kind();
Self {
location: SourceLocation::from(location),
error_kind,
phantom: PhantomData,
}
}
}
impl<'a> Diagnostic for ValidationError<'a> {
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
let label = miette::LabeledSpan::underline(self.location.source_loc.as_ref()?.span);
Some(Box::new(std::iter::once(label)))
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.location.source_loc.as_ref()?.src)
}
fn code<'s>(&'s self) -> Option<Box<dyn std::fmt::Display + 's>> {
self.error_kind.code()
}
fn severity(&self) -> Option<miette::Severity> {
self.error_kind.severity()
}
fn url<'s>(&'s self) -> Option<Box<dyn std::fmt::Display + 's>> {
self.error_kind.url()
}
fn help<'s>(&'s self) -> Option<Box<dyn std::fmt::Display + 's>> {
self.error_kind.help()
}
fn related<'s>(&'s self) -> Option<Box<dyn Iterator<Item = &'s dyn Diagnostic> + 's>> {
self.error_kind.related()
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.error_kind.diagnostic_source()
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct SourceLocation<'a> {
policy_id: PolicyId,
source_loc: Option<parser::Loc>,
phantom: PhantomData<&'a ()>,
}
impl<'a> SourceLocation<'a> {
pub fn policy_id(&self) -> &PolicyId {
&self.policy_id
}
pub fn range_start(&self) -> Option<usize> {
self.source_loc.as_ref().map(parser::Loc::start)
}
pub fn range_end(&self) -> Option<usize> {
self.source_loc.as_ref().map(parser::Loc::end)
}
}
impl<'a> std::fmt::Display for SourceLocation<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "policy `{}`", self.policy_id)?;
if let Some(loc) = &self.source_loc {
write!(f, " at offset {}-{}", loc.start(), loc.end())?;
}
Ok(())
}
}
impl<'a> From<cedar_policy_validator::SourceLocation<'a>> for SourceLocation<'static> {
fn from(loc: cedar_policy_validator::SourceLocation<'a>) -> SourceLocation<'static> {
let policy_id = PolicyId(loc.policy_id().clone());
let source_loc = loc.source_loc().cloned();
Self {
policy_id,
source_loc,
phantom: PhantomData,
}
}
}
pub fn confusable_string_checker<'a>(
templates: impl Iterator<Item = &'a Template> + 'a,
) -> impl Iterator<Item = ValidationWarning<'static>> + 'a {
cedar_policy_validator::confusable_string_checks(templates.map(|t| &t.ast))
.map(std::convert::Into::into)
}
#[derive(Debug, Clone, Error)]
#[error("validation warning on {location}: {kind}")]
pub struct ValidationWarning<'a> {
location: SourceLocation<'static>,
kind: ValidationWarningKind,
phantom: PhantomData<&'a ()>,
}
impl<'a> ValidationWarning<'a> {
pub fn warning_kind(&self) -> &ValidationWarningKind {
&self.kind
}
pub fn location(&self) -> &SourceLocation<'a> {
&self.location
}
}
#[doc(hidden)]
impl<'a> From<cedar_policy_validator::ValidationWarning<'a>> for ValidationWarning<'static> {
fn from(w: cedar_policy_validator::ValidationWarning<'a>) -> Self {
let (loc, kind) = w.to_kind_and_location();
ValidationWarning {
location: loc.into(),
kind,
phantom: PhantomData,
}
}
}
impl<'a> Diagnostic for ValidationWarning<'a> {
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
let label = miette::LabeledSpan::underline(self.location.source_loc.as_ref()?.span);
Some(Box::new(std::iter::once(label)))
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
Some(&self.location.source_loc.as_ref()?.src)
}
fn code<'s>(&'s self) -> Option<Box<dyn std::fmt::Display + 's>> {
self.kind.code()
}
fn severity(&self) -> Option<miette::Severity> {
self.kind.severity()
}
fn url<'s>(&'s self) -> Option<Box<dyn std::fmt::Display + 's>> {
self.kind.url()
}
fn help<'s>(&'s self) -> Option<Box<dyn std::fmt::Display + 's>> {
self.kind.help()
}
fn related<'s>(&'s self) -> Option<Box<dyn Iterator<Item = &'s dyn Diagnostic> + 's>> {
self.kind.related()
}
fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.kind.diagnostic_source()
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
pub struct EntityId(ast::Eid);
impl EntityId {
pub fn new(src: impl AsRef<str>) -> Self {
match src.as_ref().parse() {
Ok(eid) => eid,
Err(infallible) => match infallible {},
}
}
pub fn escaped(&self) -> SmolStr {
self.0.escaped()
}
}
impl FromStr for EntityId {
type Err = Infallible;
fn from_str(eid_str: &str) -> Result<Self, Self::Err> {
Ok(Self(ast::Eid::new(eid_str)))
}
}
impl AsRef<str> for EntityId {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl std::fmt::Display for EntityId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
pub struct EntityTypeName(ast::Name);
impl EntityTypeName {
pub fn basename(&self) -> &str {
self.0.basename().as_ref()
}
pub fn namespace_components(&self) -> impl Iterator<Item = &str> {
self.0.namespace_components().map(AsRef::as_ref)
}
pub fn namespace(&self) -> String {
self.0.namespace()
}
}
impl FromStr for EntityTypeName {
type Err = ParseErrors;
fn from_str(namespace_type_str: &str) -> Result<Self, Self::Err> {
ast::Name::from_normalized_str(namespace_type_str).map(EntityTypeName)
}
}
impl std::fmt::Display for EntityTypeName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct EntityNamespace(ast::Name);
impl FromStr for EntityNamespace {
type Err = ParseErrors;
fn from_str(namespace_str: &str) -> Result<Self, Self::Err> {
ast::Name::from_normalized_str(namespace_str).map(EntityNamespace)
}
}
impl std::fmt::Display for EntityNamespace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[repr(transparent)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, RefCast)]
pub struct EntityUid(ast::EntityUID);
impl EntityUid {
pub fn type_name(&self) -> &EntityTypeName {
#[allow(clippy::panic)]
match self.0.entity_type() {
ast::EntityType::Unspecified => panic!("Impossible to have an unspecified entity"),
ast::EntityType::Specified(name) => EntityTypeName::ref_cast(name),
}
}
pub fn id(&self) -> &EntityId {
EntityId::ref_cast(self.0.eid())
}
pub fn from_type_name_and_id(name: EntityTypeName, id: EntityId) -> Self {
Self(ast::EntityUID::from_components(name.0, id.0, None))
}
#[allow(clippy::result_large_err)]
pub fn from_json(json: serde_json::Value) -> Result<Self, impl miette::Diagnostic> {
let parsed: cedar_policy_core::entities::EntityUidJson = serde_json::from_value(json)?;
Ok::<Self, cedar_policy_core::entities::JsonDeserializationError>(Self(
parsed.into_euid(|| JsonDeserializationErrorContext::EntityUid)?,
))
}
#[cfg(test)]
pub(crate) fn from_strs(typename: &str, id: &str) -> Self {
Self::from_type_name_and_id(
EntityTypeName::from_str(typename).unwrap(),
EntityId::from_str(id).unwrap(),
)
}
}
impl FromStr for EntityUid {
type Err = ParseErrors;
fn from_str(uid_str: &str) -> Result<Self, Self::Err> {
ast::EntityUID::from_normalized_str(uid_str).map(EntityUid)
}
}
impl std::fmt::Display for EntityUid {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Diagnostic, Error)]
#[non_exhaustive]
pub enum PolicySetError {
#[error("duplicate template or policy id `{id}`")]
AlreadyDefined {
id: PolicyId,
},
#[error("unable to link template: {0}")]
#[diagnostic(transparent)]
LinkingError(#[from] ast::LinkingError),
#[error("expected a static policy, but a template-linked policy was provided")]
ExpectedStatic,
#[error("expected a template, but a static policy was provided")]
ExpectedTemplate,
#[error("unable to remove static policy `{0}` because it does not exist")]
PolicyNonexistentError(PolicyId),
#[error("unable to remove policy template `{0}` because it does not exist")]
TemplateNonexistentError(PolicyId),
#[error("unable to remove policy template `{0}` because it has active links")]
RemoveTemplateWithActiveLinksError(PolicyId),
#[error("unable to remove policy template `{0}` because it is not a template")]
RemoveTemplateNotTemplateError(PolicyId),
#[error("unable to unlink policy template `{0}` because it does not exist")]
LinkNonexistentError(PolicyId),
#[error("unable to unlink `{0}` because it is not a link")]
UnlinkLinkNotLinkError(PolicyId),
#[error("Error deserializing a policy/template from JSON: {0}")]
#[diagnostic(transparent)]
FromJson(#[from] cedar_policy_core::est::FromJsonError),
#[error("Error serializing a policy to JSON: {0}")]
#[diagnostic(transparent)]
ToJson(#[from] PolicyToJsonError),
#[error("Error serializing or deserializng from JSON: {0})")]
Json(#[from] serde_json::Error),
}
impl From<ast::PolicySetError> for PolicySetError {
fn from(e: ast::PolicySetError) -> Self {
match e {
ast::PolicySetError::Occupied { id } => Self::AlreadyDefined { id: PolicyId(id) },
}
}
}
impl From<ast::UnexpectedSlotError> for PolicySetError {
fn from(_: ast::UnexpectedSlotError) -> Self {
Self::ExpectedStatic
}
}
#[derive(Debug, Clone, Default)]
pub struct PolicySet {
pub(crate) ast: ast::PolicySet,
policies: HashMap<PolicyId, Policy>,
templates: HashMap<PolicyId, Template>,
}
impl PartialEq for PolicySet {
fn eq(&self, other: &Self) -> bool {
self.ast.eq(&other.ast)
}
}
impl Eq for PolicySet {}
impl FromStr for PolicySet {
type Err = ParseErrors;
fn from_str(policies: &str) -> Result<Self, Self::Err> {
let (texts, pset) = parser::parse_policyset_and_also_return_policy_text(policies)?;
#[allow(clippy::expect_used)]
let policies = pset.policies().map(|p|
(
PolicyId(p.id().clone()),
Policy { lossless: LosslessPolicy::policy_or_template_text(*texts.get(p.id()).expect("internal invariant violation: policy id exists in asts but not texts")), ast: p.clone() }
)
).collect();
#[allow(clippy::expect_used)]
let templates = pset.templates().map(|t|
(
PolicyId(t.id().clone()),
Template { lossless: LosslessPolicy::policy_or_template_text(*texts.get(t.id()).expect("internal invariant violation: template id exists in asts but not ests")), ast: t.clone() }
)
).collect();
Ok(Self {
ast: pset,
policies,
templates,
})
}
}
impl PolicySet {
fn from_est(est: &est::PolicySet) -> Result<Self, PolicySetError> {
let ast: ast::PolicySet = est.clone().try_into()?;
#[allow(clippy::expect_used)]
let policies = ast
.policies()
.map(|p| {
(
PolicyId::new(p.id().clone()),
Policy {
lossless: LosslessPolicy::Est(est.get_policy(p.id()).expect(
"internal invariant violation: policy id exists in asts but not ests",
)),
ast: p.clone(),
},
)
})
.collect();
#[allow(clippy::expect_used)]
let templates = ast
.templates()
.map(|t| {
(
PolicyId::new(t.id().clone()),
Template {
lossless: LosslessPolicy::Est(est.get_template(t.id()).expect(
"internal invariant violation: template id exists in asts but not ests",
)),
ast: t.clone(),
},
)
})
.collect();
Ok(Self {
ast,
policies,
templates,
})
}
pub fn from_json_str(src: impl AsRef<str>) -> Result<Self, PolicySetError> {
let est: est::PolicySet = serde_json::from_str(src.as_ref())?;
Self::from_est(&est)
}
pub fn from_json_value(src: serde_json::Value) -> Result<Self, PolicySetError> {
let est: est::PolicySet = serde_json::from_value(src)?;
Self::from_est(&est)
}
pub fn from_json_file(r: impl std::io::Read) -> Result<Self, PolicySetError> {
let est: est::PolicySet = serde_json::from_reader(r)?;
Self::from_est(&est)
}
pub fn to_json(self) -> Result<serde_json::Value, PolicySetError> {
let est = self.est()?;
let value = serde_json::to_value(est)?;
Ok(value)
}
fn est(self) -> Result<est::PolicySet, PolicyToJsonError> {
let (static_policies, template_links): (Vec<_>, Vec<_>) =
fold_partition(self.policies, is_static_or_link)?;
let static_policies = static_policies.into_iter().collect::<HashMap<_, _>>();
let templates = self
.templates
.into_iter()
.map(|(id, template)| template.lossless.est().map(|est| (id.0, est)))
.collect::<Result<HashMap<_, _>, _>>()?;
let est = est::PolicySet {
templates,
static_policies,
template_links,
};
Ok(est)
}
pub fn new() -> Self {
Self {
ast: ast::PolicySet::new(),
policies: HashMap::new(),
templates: HashMap::new(),
}
}
pub fn from_policies(
policies: impl IntoIterator<Item = Policy>,
) -> Result<Self, PolicySetError> {
let mut set = Self::new();
for policy in policies {
set.add(policy)?;
}
Ok(set)
}
pub fn add(&mut self, policy: Policy) -> Result<(), PolicySetError> {
if policy.is_static() {
let id = PolicyId(policy.ast.id().clone());
self.ast.add(policy.ast.clone())?;
self.policies.insert(id, policy);
Ok(())
} else {
Err(PolicySetError::ExpectedStatic)
}
}
pub fn remove_static(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
let Some(policy) = self.policies.remove(&policy_id) else {
return Err(PolicySetError::PolicyNonexistentError(policy_id));
};
if self
.ast
.remove_static(&ast::PolicyID::from_string(&policy_id))
.is_ok()
{
Ok(policy)
} else {
self.policies.insert(policy_id.clone(), policy);
Err(PolicySetError::PolicyNonexistentError(policy_id.clone()))
}
}
pub fn add_template(&mut self, template: Template) -> Result<(), PolicySetError> {
let id = PolicyId(template.ast.id().clone());
self.ast.add_template(template.ast.clone())?;
self.templates.insert(id, template);
Ok(())
}
pub fn remove_template(&mut self, template_id: PolicyId) -> Result<Template, PolicySetError> {
let Some(template) = self.templates.remove(&template_id) else {
return Err(PolicySetError::TemplateNonexistentError(template_id));
};
#[allow(clippy::panic)]
match self
.ast
.remove_template(&ast::PolicyID::from_string(&template_id))
{
Ok(_) => Ok(template),
Err(ast::PolicySetTemplateRemovalError::RemoveTemplateWithLinksError(_)) => {
self.templates.insert(template_id.clone(), template);
Err(PolicySetError::RemoveTemplateWithActiveLinksError(
template_id,
))
}
Err(ast::PolicySetTemplateRemovalError::NotTemplateError(_)) => {
self.templates.insert(template_id.clone(), template);
Err(PolicySetError::RemoveTemplateNotTemplateError(template_id))
}
Err(ast::PolicySetTemplateRemovalError::RemovePolicyNoTemplateError(_)) => {
panic!("Found template policy in self.templates but not in self.ast");
}
}
}
pub fn get_linked_policies(
&self,
template_id: PolicyId,
) -> Result<impl Iterator<Item = &PolicyId>, PolicySetError> {
self.ast
.get_linked_policies(&ast::PolicyID::from_string(&template_id))
.map_or_else(
|_| Err(PolicySetError::TemplateNonexistentError(template_id)),
|v| Ok(v.map(PolicyId::ref_cast)),
)
}
pub fn policies(&self) -> impl Iterator<Item = &Policy> {
self.policies.values()
}
pub fn templates(&self) -> impl Iterator<Item = &Template> {
self.templates.values()
}
pub fn template(&self, id: &PolicyId) -> Option<&Template> {
self.templates.get(id)
}
pub fn policy(&self, id: &PolicyId) -> Option<&Policy> {
self.policies.get(id)
}
pub fn annotation<'a>(&'a self, id: &PolicyId, key: impl AsRef<str>) -> Option<&'a str> {
self.ast
.get(&id.0)?
.annotation(&key.as_ref().parse().ok()?)
.map(AsRef::as_ref)
}
pub fn template_annotation(&self, id: &PolicyId, key: impl AsRef<str>) -> Option<String> {
self.ast
.get_template(&id.0)?
.annotation(&key.as_ref().parse().ok()?)
.map(|annot| annot.val.to_string())
}
pub fn is_empty(&self) -> bool {
debug_assert_eq!(
self.ast.is_empty(),
self.policies.is_empty() && self.templates.is_empty()
);
self.ast.is_empty()
}
pub fn num_of_policies(&self) -> usize {
self.policies.len()
}
pub fn num_of_templates(&self) -> usize {
self.templates.len()
}
#[allow(clippy::needless_pass_by_value)]
pub fn link(
&mut self,
template_id: PolicyId,
new_id: PolicyId,
vals: HashMap<SlotId, EntityUid>,
) -> Result<(), PolicySetError> {
let unwrapped_vals: HashMap<ast::SlotId, ast::EntityUID> = vals
.into_iter()
.map(|(key, value)| (key.into(), value.0))
.collect();
let Some(template) = self.templates.get(&template_id) else {
return Err(if self.policies.contains_key(&template_id) {
PolicySetError::ExpectedTemplate
} else {
PolicySetError::LinkingError(ast::LinkingError::NoSuchTemplate {
id: template_id.0,
})
});
};
let linked_ast = self
.ast
.link(
template_id.0.clone(),
new_id.0.clone(),
unwrapped_vals.clone(),
)
.map_err(PolicySetError::LinkingError)?;
#[allow(clippy::expect_used)]
let linked_lossless = template
.lossless
.clone()
.link(unwrapped_vals.iter().map(|(k, v)| (*k, v)))
.expect("ast.link() didn't fail above, so this shouldn't fail");
self.policies.insert(
new_id,
Policy {
ast: linked_ast.clone(),
lossless: linked_lossless,
},
);
Ok(())
}
#[doc = include_str!("../experimental_warning.md")]
#[cfg(feature = "partial-eval")]
pub fn unknown_entities(&self) -> HashSet<EntityUid> {
let mut entity_uids = HashSet::new();
for policy in self.policies.values() {
entity_uids.extend(policy.unknown_entities());
}
entity_uids
}
pub fn unlink(&mut self, policy_id: PolicyId) -> Result<Policy, PolicySetError> {
let Some(policy) = self.policies.remove(&policy_id) else {
return Err(PolicySetError::LinkNonexistentError(policy_id));
};
#[allow(clippy::panic)]
match self.ast.unlink(&ast::PolicyID::from_string(&policy_id)) {
Ok(_) => Ok(policy),
Err(ast::PolicySetUnlinkError::NotLinkError(_)) => {
self.policies.insert(policy_id.clone(), policy);
Err(PolicySetError::UnlinkLinkNotLinkError(policy_id))
}
Err(ast::PolicySetUnlinkError::UnlinkingError(_)) => {
panic!("Found linked policy in self.policies but not in self.ast")
}
}
}
}
impl std::fmt::Display for PolicySet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.policies().map(|p| &p.lossless).join("\n"))
}
}
fn is_static_or_link(
(id, policy): (PolicyId, Policy),
) -> Result<Either<(ast::PolicyID, est::Policy), est::TemplateLink>, PolicyToJsonError> {
match policy.template_id() {
Some(template_id) => {
let values = policy
.ast
.env()
.iter()
.map(|(id, euid)| (*id, euid.clone()))
.collect();
Ok(Either::Right(est::TemplateLink {
new_id: id.0,
template_id: template_id.clone().0,
values,
}))
}
None => policy.lossless.est().map(|est| Either::Left((id.0, est))),
}
}
fn fold_partition<T, A, B, E>(
i: impl IntoIterator<Item = T>,
f: impl Fn(T) -> Result<Either<A, B>, E>,
) -> Result<(Vec<A>, Vec<B>), E> {
let mut lefts = vec![];
let mut rights = vec![];
for item in i {
match f(item)? {
Either::Left(left) => lefts.push(left),
Either::Right(right) => rights.push(right),
}
}
Ok((lefts, rights))
}
#[derive(Debug, Clone)]
pub struct Template {
ast: ast::Template,
lossless: LosslessPolicy,
}
impl PartialEq for Template {
fn eq(&self, other: &Self) -> bool {
self.ast.eq(&other.ast)
}
}
impl Eq for Template {}
impl Template {
pub fn parse(id: Option<String>, src: impl AsRef<str>) -> Result<Self, ParseErrors> {
let ast = parser::parse_template(id, src.as_ref())?;
Ok(Self {
ast,
lossless: LosslessPolicy::policy_or_template_text(src.as_ref()),
})
}
pub fn id(&self) -> &PolicyId {
PolicyId::ref_cast(self.ast.id())
}
#[must_use]
pub fn new_id(&self, id: PolicyId) -> Self {
Self {
ast: self.ast.new_id(id.0),
lossless: self.lossless.clone(), }
}
pub fn effect(&self) -> Effect {
self.ast.effect()
}
pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
self.ast
.annotation(&key.as_ref().parse().ok()?)
.map(AsRef::as_ref)
}
pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
self.ast
.annotations()
.map(|(k, v)| (k.as_ref(), v.as_ref()))
}
pub fn slots(&self) -> impl Iterator<Item = &SlotId> {
self.ast.slots().map(|slot| SlotId::ref_cast(&slot.id))
}
pub fn principal_constraint(&self) -> TemplatePrincipalConstraint {
match self.ast.principal_constraint().as_inner() {
ast::PrincipalOrResourceConstraint::Any => TemplatePrincipalConstraint::Any,
ast::PrincipalOrResourceConstraint::In(eref) => {
TemplatePrincipalConstraint::In(match eref {
ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
ast::EntityReference::Slot => None,
})
}
ast::PrincipalOrResourceConstraint::Eq(eref) => {
TemplatePrincipalConstraint::Eq(match eref {
ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
ast::EntityReference::Slot => None,
})
}
ast::PrincipalOrResourceConstraint::Is(entity_type) => {
TemplatePrincipalConstraint::Is(EntityTypeName(entity_type.clone()))
}
ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
TemplatePrincipalConstraint::IsIn(
EntityTypeName(entity_type.clone()),
match eref {
ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
ast::EntityReference::Slot => None,
},
)
}
}
}
pub fn action_constraint(&self) -> ActionConstraint {
match self.ast.action_constraint() {
ast::ActionConstraint::Any => ActionConstraint::Any,
ast::ActionConstraint::In(ids) => ActionConstraint::In(
ids.iter()
.map(|id| EntityUid(id.as_ref().clone()))
.collect(),
),
ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid(id.as_ref().clone())),
}
}
pub fn resource_constraint(&self) -> TemplateResourceConstraint {
match self.ast.resource_constraint().as_inner() {
ast::PrincipalOrResourceConstraint::Any => TemplateResourceConstraint::Any,
ast::PrincipalOrResourceConstraint::In(eref) => {
TemplateResourceConstraint::In(match eref {
ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
ast::EntityReference::Slot => None,
})
}
ast::PrincipalOrResourceConstraint::Eq(eref) => {
TemplateResourceConstraint::Eq(match eref {
ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
ast::EntityReference::Slot => None,
})
}
ast::PrincipalOrResourceConstraint::Is(entity_type) => {
TemplateResourceConstraint::Is(EntityTypeName(entity_type.clone()))
}
ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
TemplateResourceConstraint::IsIn(
EntityTypeName(entity_type.clone()),
match eref {
ast::EntityReference::EUID(e) => Some(EntityUid(e.as_ref().clone())),
ast::EntityReference::Slot => None,
},
)
}
}
}
pub fn from_json(
id: Option<PolicyId>,
json: serde_json::Value,
) -> Result<Self, cedar_policy_core::est::FromJsonError> {
let est: est::Policy =
serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
Self::from_est(id, est)
}
fn from_est(
id: Option<PolicyId>,
est: est::Policy,
) -> Result<Self, cedar_policy_core::est::FromJsonError> {
Ok(Self {
ast: est.clone().try_into_ast_template(id.map(|id| id.0))?,
lossless: LosslessPolicy::Est(est),
})
}
pub fn to_json(&self) -> Result<serde_json::Value, impl miette::Diagnostic> {
let est = self.lossless.est()?;
let json = serde_json::to_value(est)?;
Ok::<_, PolicyToJsonError>(json)
}
}
impl std::fmt::Display for Template {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.lossless.fmt(f)
}
}
impl FromStr for Template {
type Err = ParseErrors;
fn from_str(src: &str) -> Result<Self, Self::Err> {
Self::parse(None, src)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PrincipalConstraint {
Any,
In(EntityUid),
Eq(EntityUid),
Is(EntityTypeName),
IsIn(EntityTypeName, EntityUid),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplatePrincipalConstraint {
Any,
In(Option<EntityUid>),
Eq(Option<EntityUid>),
Is(EntityTypeName),
IsIn(EntityTypeName, Option<EntityUid>),
}
impl TemplatePrincipalConstraint {
pub fn has_slot(&self) -> bool {
match self {
Self::Any | Self::Is(_) => false,
Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ActionConstraint {
Any,
In(Vec<EntityUid>),
Eq(EntityUid),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResourceConstraint {
Any,
In(EntityUid),
Eq(EntityUid),
Is(EntityTypeName),
IsIn(EntityTypeName, EntityUid),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TemplateResourceConstraint {
Any,
In(Option<EntityUid>),
Eq(Option<EntityUid>),
Is(EntityTypeName),
IsIn(EntityTypeName, Option<EntityUid>),
}
impl TemplateResourceConstraint {
pub fn has_slot(&self) -> bool {
match self {
Self::Any | Self::Is(_) => false,
Self::In(o) | Self::Eq(o) | Self::IsIn(_, o) => o.is_none(),
}
}
}
#[repr(transparent)]
#[derive(Debug, PartialEq, Eq, Clone, Hash, Serialize, Deserialize, RefCast)]
pub struct PolicyId(ast::PolicyID);
impl PolicyId {
pub fn new(id: impl AsRef<str>) -> Self {
Self(ast::PolicyID::from_string(id.as_ref()))
}
}
impl FromStr for PolicyId {
type Err = ParseErrors;
fn from_str(id: &str) -> Result<Self, Self::Err> {
Ok(Self(ast::PolicyID::from_string(id)))
}
}
impl std::fmt::Display for PolicyId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for PolicyId {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct Policy {
ast: ast::Policy,
lossless: LosslessPolicy,
}
impl PartialEq for Policy {
fn eq(&self, other: &Self) -> bool {
self.ast.eq(&other.ast)
}
}
impl Eq for Policy {}
impl Policy {
pub fn template_id(&self) -> Option<&PolicyId> {
if self.is_static() {
None
} else {
Some(PolicyId::ref_cast(self.ast.template().id()))
}
}
pub fn template_links(&self) -> Option<HashMap<SlotId, EntityUid>> {
if self.is_static() {
None
} else {
let wrapped_vals: HashMap<SlotId, EntityUid> = self
.ast
.env()
.iter()
.map(|(key, value)| (SlotId(*key), EntityUid(value.clone())))
.collect();
Some(wrapped_vals)
}
}
pub fn effect(&self) -> Effect {
self.ast.effect()
}
pub fn annotation(&self, key: impl AsRef<str>) -> Option<&str> {
self.ast
.annotation(&key.as_ref().parse().ok()?)
.map(AsRef::as_ref)
}
pub fn annotations(&self) -> impl Iterator<Item = (&str, &str)> {
self.ast
.annotations()
.map(|(k, v)| (k.as_ref(), v.as_ref()))
}
pub fn id(&self) -> &PolicyId {
PolicyId::ref_cast(self.ast.id())
}
#[must_use]
pub fn new_id(&self, id: PolicyId) -> Self {
Self {
ast: self.ast.new_id(id.0),
lossless: self.lossless.clone(), }
}
pub fn is_static(&self) -> bool {
self.ast.is_static()
}
pub fn principal_constraint(&self) -> PrincipalConstraint {
let slot_id = ast::SlotId::principal();
match self.ast.template().principal_constraint().as_inner() {
ast::PrincipalOrResourceConstraint::Any => PrincipalConstraint::Any,
ast::PrincipalOrResourceConstraint::In(eref) => {
PrincipalConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
}
ast::PrincipalOrResourceConstraint::Eq(eref) => {
PrincipalConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
}
ast::PrincipalOrResourceConstraint::Is(entity_type) => {
PrincipalConstraint::Is(EntityTypeName(entity_type.clone()))
}
ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
PrincipalConstraint::IsIn(
EntityTypeName(entity_type.clone()),
self.convert_entity_reference(eref, slot_id).clone(),
)
}
}
}
pub fn action_constraint(&self) -> ActionConstraint {
match self.ast.template().action_constraint() {
ast::ActionConstraint::Any => ActionConstraint::Any,
ast::ActionConstraint::In(ids) => ActionConstraint::In(
ids.iter()
.map(|euid| EntityUid::ref_cast(euid.as_ref()))
.cloned()
.collect(),
),
ast::ActionConstraint::Eq(id) => ActionConstraint::Eq(EntityUid::ref_cast(id).clone()),
}
}
pub fn resource_constraint(&self) -> ResourceConstraint {
let slot_id = ast::SlotId::resource();
match self.ast.template().resource_constraint().as_inner() {
ast::PrincipalOrResourceConstraint::Any => ResourceConstraint::Any,
ast::PrincipalOrResourceConstraint::In(eref) => {
ResourceConstraint::In(self.convert_entity_reference(eref, slot_id).clone())
}
ast::PrincipalOrResourceConstraint::Eq(eref) => {
ResourceConstraint::Eq(self.convert_entity_reference(eref, slot_id).clone())
}
ast::PrincipalOrResourceConstraint::Is(entity_type) => {
ResourceConstraint::Is(EntityTypeName(entity_type.clone()))
}
ast::PrincipalOrResourceConstraint::IsIn(entity_type, eref) => {
ResourceConstraint::IsIn(
EntityTypeName(entity_type.clone()),
self.convert_entity_reference(eref, slot_id).clone(),
)
}
}
}
fn convert_entity_reference<'a>(
&'a self,
r: &'a ast::EntityReference,
slot: ast::SlotId,
) -> &'a EntityUid {
match r {
ast::EntityReference::EUID(euid) => EntityUid::ref_cast(euid),
#[allow(clippy::unwrap_used)]
ast::EntityReference::Slot => EntityUid::ref_cast(self.ast.env().get(&slot).unwrap()),
}
}
pub fn parse(id: Option<String>, policy_src: impl AsRef<str>) -> Result<Self, ParseErrors> {
let inline_ast = parser::parse_policy(id, policy_src.as_ref())?;
let (_, ast) = ast::Template::link_static_policy(inline_ast);
Ok(Self {
ast,
lossless: LosslessPolicy::policy_or_template_text(policy_src.as_ref()),
})
}
pub fn from_json(
id: Option<PolicyId>,
json: serde_json::Value,
) -> Result<Self, cedar_policy_core::est::FromJsonError> {
let est: est::Policy =
serde_json::from_value(json).map_err(JsonDeserializationError::Serde)?;
Self::from_est(id, est)
}
fn from_est(
id: Option<PolicyId>,
est: est::Policy,
) -> Result<Self, cedar_policy_core::est::FromJsonError> {
Ok(Self {
ast: est.clone().try_into_ast_policy(id.map(|id| id.0))?,
lossless: LosslessPolicy::Est(est),
})
}
pub fn to_json(&self) -> Result<serde_json::Value, impl miette::Diagnostic> {
let est = self.lossless.est()?;
let json = serde_json::to_value(est)?;
Ok::<_, PolicyToJsonError>(json)
}
#[doc = include_str!("../experimental_warning.md")]
#[cfg(feature = "partial-eval")]
pub fn unknown_entities(&self) -> HashSet<EntityUid> {
self.ast
.condition()
.unknowns()
.filter_map(
|ast::Unknown {
name,
type_annotation,
}| {
if matches!(type_annotation, Some(ast::Type::Entity { .. })) {
EntityUid::from_str(name.as_str()).ok()
} else {
None
}
},
)
.collect()
}
#[cfg_attr(not(feature = "partial-eval"), allow(unused))]
pub(crate) fn from_ast(ast: ast::Policy) -> Self {
let text = ast.to_string(); Self {
ast,
lossless: LosslessPolicy::policy_or_template_text(text),
}
}
}
impl std::fmt::Display for Policy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.lossless.fmt(f)
}
}
impl FromStr for Policy {
type Err = ParseErrors;
fn from_str(policy: &str) -> Result<Self, Self::Err> {
Self::parse(None, policy)
}
}
#[derive(Debug, Clone)]
enum LosslessPolicy {
Est(est::Policy),
Text {
text: String,
slots: HashMap<ast::SlotId, ast::EntityUID>,
},
}
impl LosslessPolicy {
fn policy_or_template_text(text: impl Into<String>) -> Self {
Self::Text {
text: text.into(),
slots: HashMap::new(),
}
}
fn est(&self) -> Result<est::Policy, PolicyToJsonError> {
match self {
Self::Est(est) => Ok(est.clone()),
Self::Text { text, slots } => {
let est = parser::parse_policy_or_template_to_est(text)?;
if slots.is_empty() {
Ok(est)
} else {
let unwrapped_vals = slots.iter().map(|(k, v)| (*k, v.into())).collect();
Ok(est.link(&unwrapped_vals)?)
}
}
}
}
fn link<'a>(
self,
vals: impl IntoIterator<Item = (ast::SlotId, &'a ast::EntityUID)>,
) -> Result<Self, est::InstantiationError> {
match self {
Self::Est(est) => {
let unwrapped_est_vals: HashMap<
ast::SlotId,
cedar_policy_core::entities::EntityUidJson,
> = vals.into_iter().map(|(k, v)| (k, v.into())).collect();
Ok(Self::Est(est.link(&unwrapped_est_vals)?))
}
Self::Text { text, slots } => {
debug_assert!(
slots.is_empty(),
"shouldn't call link() on an already-linked policy"
);
let slots = vals.into_iter().map(|(k, v)| (k, v.clone())).collect();
Ok(Self::Text { text, slots })
}
}
}
}
impl std::fmt::Display for LosslessPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Est(est) => write!(f, "{est}"),
Self::Text { text, slots } => {
if slots.is_empty() {
write!(f, "{text}")
} else {
match self.est() {
Ok(est) => write!(f, "{est}"),
Err(e) => write!(f, "<invalid linked policy: {e}>"),
}
}
}
}
}
}
#[derive(Debug, Diagnostic, Error)]
pub enum PolicyToJsonError {
#[error(transparent)]
#[diagnostic(transparent)]
Parse(#[from] ParseErrors),
#[error(transparent)]
#[diagnostic(transparent)]
Link(#[from] est::InstantiationError),
#[error(transparent)]
Serde(#[from] serde_json::Error),
}
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
pub struct Expression(ast::Expr);
impl Expression {
pub fn new_string(value: String) -> Self {
Self(ast::Expr::val(value))
}
pub fn new_bool(value: bool) -> Self {
Self(ast::Expr::val(value))
}
pub fn new_long(value: Integer) -> Self {
Self(ast::Expr::val(value))
}
pub fn new_record(
fields: impl IntoIterator<Item = (String, Self)>,
) -> Result<Self, ExprConstructionError> {
Ok(Self(ast::Expr::record(
fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
)?))
}
pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
Self(ast::Expr::set(values.into_iter().map(|v| v.0)))
}
pub fn new_ip(src: impl AsRef<str>) -> Self {
let src_expr = ast::Expr::val(src.as_ref());
Self(ast::Expr::call_extension_fn(
ip_extension_name(),
vec![src_expr],
))
}
pub fn new_decimal(src: impl AsRef<str>) -> Self {
let src_expr = ast::Expr::val(src.as_ref());
Self(ast::Expr::call_extension_fn(
decimal_extension_name(),
vec![src_expr],
))
}
}
impl FromStr for Expression {
type Err = ParseErrors;
fn from_str(expression: &str) -> Result<Self, Self::Err> {
ast::Expr::from_str(expression).map(Expression)
}
}
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
pub struct RestrictedExpression(ast::RestrictedExpr);
impl RestrictedExpression {
pub fn new_string(value: String) -> Self {
Self(ast::RestrictedExpr::val(value))
}
pub fn new_bool(value: bool) -> Self {
Self(ast::RestrictedExpr::val(value))
}
pub fn new_long(value: Integer) -> Self {
Self(ast::RestrictedExpr::val(value))
}
pub fn new_entity_uid(value: EntityUid) -> Self {
Self(ast::RestrictedExpr::val(value.0))
}
pub fn new_record(
fields: impl IntoIterator<Item = (String, Self)>,
) -> Result<Self, ExprConstructionError> {
Ok(Self(ast::RestrictedExpr::record(
fields.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
)?))
}
pub fn new_set(values: impl IntoIterator<Item = Self>) -> Self {
Self(ast::RestrictedExpr::set(values.into_iter().map(|v| v.0)))
}
pub fn new_ip(src: impl AsRef<str>) -> Self {
let src_expr = ast::RestrictedExpr::val(src.as_ref());
Self(ast::RestrictedExpr::call_extension_fn(
ip_extension_name(),
[src_expr],
))
}
pub fn new_decimal(src: impl AsRef<str>) -> Self {
let src_expr = ast::RestrictedExpr::val(src.as_ref());
Self(ast::RestrictedExpr::call_extension_fn(
decimal_extension_name(),
[src_expr],
))
}
#[cfg(test)]
pub(crate) fn into_inner(self) -> ast::RestrictedExpr {
self.0
}
}
fn decimal_extension_name() -> ast::Name {
#[allow(clippy::unwrap_used)]
ast::Name::unqualified_name("decimal".parse().unwrap())
}
fn ip_extension_name() -> ast::Name {
#[allow(clippy::unwrap_used)]
ast::Name::unqualified_name("ip".parse().unwrap())
}
impl FromStr for RestrictedExpression {
type Err = RestrictedExprParseError;
fn from_str(expression: &str) -> Result<Self, Self::Err> {
ast::RestrictedExpr::from_str(expression).map(RestrictedExpression)
}
}
#[doc = include_str!("../experimental_warning.md")]
#[cfg(feature = "partial-eval")]
#[derive(Debug)]
pub struct RequestBuilder<S> {
principal: ast::EntityUIDEntry,
action: ast::EntityUIDEntry,
resource: ast::EntityUIDEntry,
context: Option<ast::Context>,
schema: S,
}
#[doc = include_str!("../experimental_warning.md")]
#[cfg(feature = "partial-eval")]
#[derive(Debug)]
pub struct UnsetSchema;
#[cfg(feature = "partial-eval")]
impl Default for RequestBuilder<UnsetSchema> {
fn default() -> Self {
Self {
principal: ast::EntityUIDEntry::Unknown { loc: None },
action: ast::EntityUIDEntry::Unknown { loc: None },
resource: ast::EntityUIDEntry::Unknown { loc: None },
context: None,
schema: UnsetSchema,
}
}
}
#[cfg(feature = "partial-eval")]
impl<S> RequestBuilder<S> {
#[must_use]
pub fn principal(self, principal: Option<EntityUid>) -> Self {
Self {
principal: match principal {
Some(p) => ast::EntityUIDEntry::concrete(p.0, None),
None => ast::EntityUIDEntry::concrete(
ast::EntityUID::unspecified_from_eid(ast::Eid::new("principal")),
None,
),
},
..self
}
}
#[must_use]
pub fn action(self, action: Option<EntityUid>) -> Self {
Self {
action: match action {
Some(a) => ast::EntityUIDEntry::concrete(a.0, None),
None => ast::EntityUIDEntry::concrete(
ast::EntityUID::unspecified_from_eid(ast::Eid::new("action")),
None,
),
},
..self
}
}
#[must_use]
pub fn resource(self, resource: Option<EntityUid>) -> Self {
Self {
resource: match resource {
Some(r) => ast::EntityUIDEntry::concrete(r.0, None),
None => ast::EntityUIDEntry::concrete(
ast::EntityUID::unspecified_from_eid(ast::Eid::new("resource")),
None,
),
},
..self
}
}
#[must_use]
pub fn context(self, context: Context) -> Self {
Self {
context: Some(context.0),
..self
}
}
}
#[cfg(feature = "partial-eval")]
impl RequestBuilder<UnsetSchema> {
#[must_use]
pub fn schema(self, schema: &Schema) -> RequestBuilder<&Schema> {
RequestBuilder {
principal: self.principal,
action: self.action,
resource: self.resource,
context: self.context,
schema,
}
}
pub fn build(self) -> Request {
Request(ast::Request::new_unchecked(
self.principal,
self.action,
self.resource,
self.context,
))
}
}
#[cfg(feature = "partial-eval")]
impl RequestBuilder<&Schema> {
pub fn build(self) -> Result<Request, RequestValidationError> {
Ok(Request(ast::Request::new_with_unknowns(
self.principal,
self.action,
self.resource,
self.context,
Some(&self.schema.0),
Extensions::all_available(),
)?))
}
}
#[repr(transparent)]
#[derive(Debug, RefCast)]
pub struct Request(pub(crate) ast::Request);
impl Request {
#[doc = include_str!("../experimental_warning.md")]
#[cfg(feature = "partial-eval")]
pub fn builder() -> RequestBuilder<UnsetSchema> {
RequestBuilder::default()
}
pub fn new(
principal: Option<EntityUid>,
action: Option<EntityUid>,
resource: Option<EntityUid>,
context: Context,
schema: Option<&Schema>,
) -> Result<Self, RequestValidationError> {
let p = match principal {
Some(p) => p.0,
None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("principal")),
};
let a = match action {
Some(a) => a.0,
None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("action")),
};
let r = match resource {
Some(r) => r.0,
None => ast::EntityUID::unspecified_from_eid(ast::Eid::new("resource")),
};
Ok(Self(ast::Request::new(
(p, None),
(a, None),
(r, None),
context.0,
schema.map(|schema| &schema.0),
Extensions::all_available(),
)?))
}
pub fn principal(&self) -> Option<&EntityUid> {
match self.0.principal() {
ast::EntityUIDEntry::Known { euid, .. } => match euid.entity_type() {
ast::EntityType::Specified(_) => Some(EntityUid::ref_cast(euid.as_ref())),
ast::EntityType::Unspecified => None,
},
ast::EntityUIDEntry::Unknown { .. } => None,
}
}
pub fn action(&self) -> Option<&EntityUid> {
match self.0.action() {
ast::EntityUIDEntry::Known { euid, .. } => match euid.entity_type() {
ast::EntityType::Specified(_) => Some(EntityUid::ref_cast(euid.as_ref())),
ast::EntityType::Unspecified => None,
},
ast::EntityUIDEntry::Unknown { .. } => None,
}
}
pub fn resource(&self) -> Option<&EntityUid> {
match self.0.resource() {
ast::EntityUIDEntry::Known { euid, .. } => match euid.entity_type() {
ast::EntityType::Specified(_) => Some(EntityUid::ref_cast(euid.as_ref())),
ast::EntityType::Unspecified => None,
},
ast::EntityUIDEntry::Unknown { .. } => None,
}
}
}
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
pub struct Context(ast::Context);
impl Context {
pub fn empty() -> Self {
Self(ast::Context::empty())
}
pub fn from_pairs(
pairs: impl IntoIterator<Item = (String, RestrictedExpression)>,
) -> Result<Self, ContextCreationError> {
Ok(Self(ast::Context::from_pairs(
pairs.into_iter().map(|(k, v)| (SmolStr::from(k), v.0)),
Extensions::all_available(),
)?))
}
pub fn from_json_str(
json: &str,
schema: Option<(&Schema, &EntityUid)>,
) -> Result<Self, ContextJsonError> {
let schema = schema
.map(|(s, uid)| Self::get_context_schema(s, uid))
.transpose()?;
let context = cedar_policy_core::entities::ContextJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
)
.from_json_str(json)?;
Ok(Self(context))
}
pub fn from_json_value(
json: serde_json::Value,
schema: Option<(&Schema, &EntityUid)>,
) -> Result<Self, ContextJsonError> {
let schema = schema
.map(|(s, uid)| Self::get_context_schema(s, uid))
.transpose()?;
let context = cedar_policy_core::entities::ContextJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
)
.from_json_value(json)?;
Ok(Self(context))
}
pub fn from_json_file(
json: impl std::io::Read,
schema: Option<(&Schema, &EntityUid)>,
) -> Result<Self, ContextJsonError> {
let schema = schema
.map(|(s, uid)| Self::get_context_schema(s, uid))
.transpose()?;
let context = cedar_policy_core::entities::ContextJsonParser::new(
schema.as_ref(),
Extensions::all_available(),
)
.from_json_file(json)?;
Ok(Self(context))
}
fn get_context_schema(
schema: &Schema,
action: &EntityUid,
) -> Result<impl ContextSchema, ContextJsonError> {
cedar_policy_validator::context_schema_for_action(&schema.0, &action.0).ok_or_else(|| {
ContextJsonError::MissingAction {
action: action.clone(),
}
})
}
pub fn merge(
self,
other_context: impl IntoIterator<Item = (String, RestrictedExpression)>,
) -> Result<Self, ContextCreationError> {
Self::from_pairs(self.into_iter().chain(other_context))
}
}
mod context {
use super::{ast, RestrictedExpression};
#[derive(Debug)]
pub struct IntoIter {
pub(super) inner: <ast::Context as IntoIterator>::IntoIter,
}
impl Iterator for IntoIter {
type Item = (String, RestrictedExpression);
fn next(&mut self) -> Option<Self::Item> {
self.inner.next().map(|(k, v)| {
(
k.to_string(),
match v {
ast::PartialValue::Value(val) => {
RestrictedExpression(ast::RestrictedExpr::from(val))
}
ast::PartialValue::Residual(exp) => {
RestrictedExpression(ast::RestrictedExpr::new_unchecked(exp))
}
},
)
})
}
}
}
impl IntoIterator for Context {
type Item = (String, RestrictedExpression);
type IntoIter = context::IntoIter;
fn into_iter(self) -> Self::IntoIter {
Self::IntoIter {
inner: self.0.into_iter(),
}
}
}
#[derive(Debug, Diagnostic, Error)]
pub enum ContextJsonError {
#[error(transparent)]
#[diagnostic(transparent)]
JsonDeserialization(#[from] ContextJsonDeserializationError),
#[error("action `{action}` does not exist in the supplied schema")]
MissingAction {
action: EntityUid,
},
}
impl std::fmt::Display for Request {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum EvalResult {
Bool(bool),
Long(Integer),
String(String),
EntityUid(EntityUid),
Set(Set),
Record(Record),
ExtensionValue(String),
}
#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct Set(BTreeSet<EvalResult>);
impl Set {
pub fn iter(&self) -> impl Iterator<Item = &EvalResult> {
self.0.iter()
}
pub fn contains(&self, elem: &EvalResult) -> bool {
self.0.contains(elem)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[derive(Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct Record(BTreeMap<String, EvalResult>);
impl Record {
pub fn iter(&self) -> impl Iterator<Item = (&String, &EvalResult)> {
self.0.iter()
}
pub fn contains_attribute(&self, key: impl AsRef<str>) -> bool {
self.0.contains_key(key.as_ref())
}
pub fn get(&self, key: impl AsRef<str>) -> Option<&EvalResult> {
self.0.get(key.as_ref())
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
#[doc(hidden)]
impl From<ast::Value> for EvalResult {
fn from(v: ast::Value) -> Self {
match v.value {
ast::ValueKind::Lit(ast::Literal::Bool(b)) => Self::Bool(b),
ast::ValueKind::Lit(ast::Literal::Long(i)) => Self::Long(i),
ast::ValueKind::Lit(ast::Literal::String(s)) => Self::String(s.to_string()),
ast::ValueKind::Lit(ast::Literal::EntityUID(e)) => {
Self::EntityUid(EntityUid(ast::EntityUID::clone(&e)))
}
ast::ValueKind::Set(set) => Self::Set(Set(set
.authoritative
.iter()
.map(|v| v.clone().into())
.collect())),
ast::ValueKind::Record(record) => Self::Record(Record(
record
.iter()
.map(|(k, v)| (k.to_string(), v.clone().into()))
.collect(),
)),
ast::ValueKind::ExtensionValue(ev) => Self::ExtensionValue(ev.to_string()),
}
}
}
impl std::fmt::Display for EvalResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Bool(b) => write!(f, "{b}"),
Self::Long(l) => write!(f, "{l}"),
Self::String(s) => write!(f, "\"{}\"", s.escape_debug()),
Self::EntityUid(uid) => write!(f, "{uid}"),
Self::Set(s) => {
write!(f, "[")?;
for (i, ev) in s.iter().enumerate() {
write!(f, "{ev}")?;
if (i + 1) < s.len() {
write!(f, ", ")?;
}
}
write!(f, "]")?;
Ok(())
}
Self::Record(r) => {
write!(f, "{{")?;
for (i, (k, v)) in r.iter().enumerate() {
write!(f, "\"{}\": {v}", k.escape_debug())?;
if (i + 1) < r.len() {
write!(f, ", ")?;
}
}
write!(f, "}}")?;
Ok(())
}
Self::ExtensionValue(s) => write!(f, "{s}"),
}
}
}
pub fn eval_expression(
request: &Request,
entities: &Entities,
expr: &Expression,
) -> Result<EvalResult, EvaluationError> {
let all_ext = Extensions::all_available();
let eval = Evaluator::new(request.0.clone(), &entities.0, &all_ext);
Ok(EvalResult::from(
eval.interpret(&expr.0, &ast::SlotEnv::new())?,
))
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod test {
use cool_asserts::assert_matches;
use super::*;
#[test]
fn test_all_ints() {
test_single_int(0);
test_single_int(i64::MAX);
test_single_int(i64::MIN);
test_single_int(7);
test_single_int(-7);
}
fn test_single_int(x: i64) {
for i in 0..4 {
test_single_int_with_dashes(x, i);
}
}
fn test_single_int_with_dashes(x: i64, num_dashes: usize) {
let dashes = vec!['-'; num_dashes].into_iter().collect::<String>();
let src = format!(r#"permit(principal, action, resource) when {{ {dashes}{x} }};"#);
let p: Policy = src.parse().unwrap();
let json = p.to_json().unwrap();
let round_trip = Policy::from_json(None, json).unwrap();
let pretty_print = format!("{round_trip}");
assert!(pretty_print.contains(&x.to_string()));
if x != 0 {
let expected_dashes = if x < 0 { num_dashes + 1 } else { num_dashes };
assert_eq!(
pretty_print.chars().filter(|c| *c == '-').count(),
expected_dashes
);
}
}
#[test]
fn json_bignum_1() {
let src = r#"
permit(
principal,
action == Action::"action",
resource
) when {
-9223372036854775808
};"#;
let p: Policy = src.parse().unwrap();
p.to_json().unwrap();
}
#[test]
fn json_bignum_1a() {
let src = r"
permit(principal, action, resource) when {
(true && (-90071992547409921)) && principal
};";
let p: Policy = src.parse().unwrap();
let v = p.to_json().unwrap();
let s = serde_json::to_string(&v).unwrap();
assert!(s.contains("90071992547409921"));
}
#[test]
fn json_bignum_2() {
let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":90071992547409921}}}}]}"#;
let v: serde_json::Value = serde_json::from_str(src).unwrap();
let p = Policy::from_json(None, v).unwrap();
let pretty = format!("{p}");
assert!(pretty.contains("90071992547409921"));
}
#[test]
fn json_bignum_2a() {
let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":-9223372036854775808}}}}]}"#;
let v: serde_json::Value = serde_json::from_str(src).unwrap();
let p = Policy::from_json(None, v).unwrap();
let pretty = format!("{p}");
assert!(pretty.contains("-9223372036854775808"));
}
#[test]
fn json_bignum_3() {
let src = r#"{"effect":"permit","principal":{"op":"All"},"action":{"op":"All"},"resource":{"op":"All"},"conditions":[{"kind":"when","body":{"==":{"left":{".":{"left":{"Var":"principal"},"attr":"x"}},"right":{"Value":9223372036854775808}}}}]}"#;
let v: serde_json::Value = serde_json::from_str(src).unwrap();
assert!(Policy::from_json(None, v).is_err());
}
#[test]
fn ip_name_correct() {
assert_eq!(ip_extension_name(), ast::Name::from_str("ip").unwrap());
}
#[test]
fn expr_ip_constructor() {
let ip = Expression::new_ip("10.10.10.10");
assert_matches!(ip.0.expr_kind(),
ast::ExprKind::ExtensionFunctionApp { fn_name, args} => {
assert_eq!(fn_name, &("ip".parse().unwrap()));
assert_eq!(args.as_ref().len(), 1);
let arg = args.first().unwrap();
assert_matches!(arg.expr_kind(),
ast::ExprKind::Lit(ast::Literal::String(s)) => s.as_str() == "10.10.10.10");
}
);
}
#[test]
fn expr_ip() {
let ip = Expression::new_ip("10.10.10.10");
assert_matches!(evaluate_empty(&ip),
Ok(EvalResult::ExtensionValue(o)) => assert_eq!(&o, "10.10.10.10/32")
);
}
#[test]
fn expr_ip_network() {
let ip = Expression::new_ip("10.10.10.10/16");
assert_matches!(evaluate_empty(&ip),
Ok(EvalResult::ExtensionValue(o)) => assert_eq!(&o, "10.10.10.10/16")
);
}
#[test]
fn expr_bad_ip() {
let ip = Expression::new_ip("192.168.312.3");
assert_matches!(evaluate_empty(&ip),
Err(e) => assert_matches!(e.error_kind(),
EvaluationErrorKind::FailedExtensionFunctionApplication {
extension_name, ..
} => assert_eq!(extension_name, &("ipaddr".parse().unwrap()))
)
);
}
#[test]
fn expr_bad_cidr() {
let ip = Expression::new_ip("192.168.0.3/100");
assert_matches!(evaluate_empty(&ip),
Err(e) => assert_matches!(e.error_kind(),
EvaluationErrorKind::FailedExtensionFunctionApplication {
extension_name, ..
} => assert_eq!(extension_name, &("ipaddr".parse().unwrap()))
)
);
}
#[test]
fn expr_nonsense_ip() {
let ip = Expression::new_ip("foobar");
assert_matches!(evaluate_empty(&ip),
Err(e) => assert_matches!(e.error_kind(),
EvaluationErrorKind::FailedExtensionFunctionApplication {
extension_name, ..
} => assert_eq!(extension_name, &("ipaddr".parse().unwrap()))
)
);
}
fn evaluate_empty(expr: &Expression) -> Result<EvalResult, EvaluationError> {
let r = Request::new(None, None, None, Context::empty(), None).unwrap();
let e = Entities::empty();
eval_expression(&r, &e, expr)
}
#[test]
fn rexpr_ip_constructor() {
let ip = RestrictedExpression::new_ip("10.10.10.10");
assert_matches!(ip.0.expr_kind(),
ast::ExprKind::ExtensionFunctionApp { fn_name, args} => {
assert_eq!(fn_name, &("ip".parse().unwrap()));
assert_eq!(args.as_ref().len(), 1);
let arg = args.first().unwrap();
assert_matches!(arg.expr_kind(),
ast::ExprKind::Lit(ast::Literal::String(s)) => s.as_str() == "10.10.10.10");
}
);
}
#[test]
fn decimal_name_correct() {
assert_eq!(
decimal_extension_name(),
ast::Name::from_str("decimal").unwrap()
);
}
#[test]
fn expr_decimal_constructor() {
let decimal = Expression::new_decimal("1234.1234");
assert_matches!(decimal.0.expr_kind(),
ast::ExprKind::ExtensionFunctionApp { fn_name, args} => {
assert_eq!(fn_name, &("decimal".parse().unwrap()));
assert_eq!(args.as_ref().len(), 1);
let arg = args.first().unwrap();
assert_matches!(arg.expr_kind(),
ast::ExprKind::Lit(ast::Literal::String(s)) => s.as_str() == "1234.1234");
}
);
}
#[test]
fn rexpr_decimal_constructor() {
let decimal = RestrictedExpression::new_decimal("1234.1234");
assert_matches!(decimal.0.expr_kind(),
ast::ExprKind::ExtensionFunctionApp { fn_name, args} => {
assert_eq!(fn_name, &("decimal".parse().unwrap()));
assert_eq!(args.as_ref().len(), 1);
let arg = args.first().unwrap();
assert_matches!(arg.expr_kind(),
ast::ExprKind::Lit(ast::Literal::String(s)) => s.as_str() == "1234.1234");
}
);
}
#[test]
fn valid_decimal() {
let decimal = Expression::new_decimal("1234.1234");
assert_matches!(evaluate_empty(&decimal),
Ok(EvalResult::ExtensionValue(s)) => s == "1234.1234");
}
#[test]
fn invalid_decimal() {
let decimal = Expression::new_decimal("1234.12345");
assert_matches!(evaluate_empty(&decimal),
Err(e) => assert_matches!(e.error_kind(),
EvaluationErrorKind::FailedExtensionFunctionApplication {
extension_name, ..
} => assert_eq!(extension_name, &("decimal".parse().unwrap()))
)
);
}
#[test]
fn into_iter_entities() {
let test_data = r#"
[
{
"uid": {"type":"User","id":"alice"},
"attrs": {
"age":19,
"ip_addr":{"__extn":{"fn":"ip", "arg":"10.0.1.101"}}
},
"parents": [{"type":"Group","id":"admin"}]
},
{
"uid": {"type":"Group","id":"admin"},
"attrs": {},
"parents": []
}
]
"#;
let list = Entities::from_json_str(test_data, None).unwrap();
let mut list_out: Vec<String> = list
.into_iter()
.map(|entity| entity.uid().id().to_string())
.collect();
list_out.sort();
assert_eq!(list_out, &["admin", "alice"]);
}
#[test]
fn test_partition_fold() {
let even_or_odd = |s: &str| {
i64::from_str_radix(s, 10).map(|i| {
if i % 2 == 0 {
Either::Left(i)
} else {
Either::Right(i)
}
})
};
let lst = ["23", "24", "75", "9320"];
let (evens, odds) = fold_partition(lst, even_or_odd).unwrap();
assert!(evens.into_iter().all(|i| i % 2 == 0));
assert!(odds.into_iter().all(|i| i % 2 != 0));
}
#[test]
fn test_partition_fold_err() {
let even_or_odd = |s: &str| {
i64::from_str_radix(s, 10).map(|i| {
if i % 2 == 0 {
Either::Left(i)
} else {
Either::Right(i)
}
})
};
let lst = ["23", "24", "not-a-number", "75", "9320"];
assert!(fold_partition(lst, even_or_odd).is_err());
}
#[test]
fn test_est_policyset_encoding() {
let mut pset = PolicySet::default();
let policy: Policy = r#"permit(principal, action, resource) when { principal.foo };"#
.parse()
.unwrap();
pset.add(policy.new_id(PolicyId::new("policy"))).unwrap();
let template: Template =
r#"permit(principal == ?principal, action, resource) when { principal.bar };"#
.parse()
.unwrap();
pset.add_template(template.new_id(PolicyId::new("template")))
.unwrap();
pset.link(
PolicyId::new("template"),
PolicyId::new("Link1"),
HashMap::from_iter([(SlotId::principal(), r#"User::"Joe""#.parse().unwrap())]),
)
.unwrap();
pset.link(
PolicyId::new("template"),
PolicyId::new("Link2"),
HashMap::from_iter([(SlotId::principal(), r#"User::"Sally""#.parse().unwrap())]),
)
.unwrap();
let json = pset.to_json().unwrap();
let pset2 = PolicySet::from_json_value(json).unwrap();
assert_eq!(pset2.policies().count(), 3);
let static_policy = pset2.policy(&PolicyId::new("policy")).unwrap();
assert!(static_policy.is_static());
let link = pset2.policy(&PolicyId::new("Link1")).unwrap();
assert!(!link.is_static());
assert_eq!(link.template_id(), Some(&PolicyId::new("template")));
assert_eq!(
link.template_links(),
Some(HashMap::from_iter([(
SlotId::principal(),
r#"User::"Joe""#.parse().unwrap()
)]))
);
let link = pset2.policy(&PolicyId::new("Link2")).unwrap();
assert!(!link.is_static());
assert_eq!(link.template_id(), Some(&PolicyId::new("template")));
assert_eq!(
link.template_links(),
Some(HashMap::from_iter([(
SlotId::principal(),
r#"User::"Sally""#.parse().unwrap()
)]))
);
let template = pset2.template(&PolicyId::new("template")).unwrap();
assert_eq!(template.slots().count(), 1);
}
#[test]
fn test_est_policyset_decoding_empty() {
let empty = serde_json::json!({
"templates" : {},
"staticPolicies" : {},
"templateLinks" : []
});
let empty = PolicySet::from_json_value(empty).unwrap();
assert_eq!(empty, PolicySet::default());
}
#[test]
fn test_est_policyset_decoding_single() {
let value = serde_json::json!({
"staticPolicies" :{
"policy1": {
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "12UA45" }
},
"action": {
"op": "==",
"entity": { "type": "Action", "id": "view" }
},
"resource": {
"op": "in",
"entity": { "type": "Folder", "id": "abc" }
},
"conditions": [
{
"kind": "when",
"body": {
"==": {
"left": {
".": {
"left": {
"Var": "context"
},
"attr": "tls_version"
}
},
"right": {
"Value": "1.3"
}
}
}
}
]
}
},
"templates" : {},
"templateLinks" : []
});
let policyset = PolicySet::from_json_value(value).unwrap();
assert_eq!(policyset.templates().count(), 0);
assert_eq!(policyset.policies().count(), 1);
assert!(policyset.policy(&PolicyId::new("policy1")).is_some());
}
#[test]
fn test_est_policyset_decoding_templates() {
let value = serde_json::json!({
"staticPolicies": {
"policy1": {
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "12UA45" }
},
"action": {
"op": "==",
"entity": { "type": "Action", "id": "view" }
},
"resource": {
"op": "in",
"entity": { "type": "Folder", "id": "abc" }
},
"conditions": [
{
"kind": "when",
"body": {
"==": {
"left": {
".": {
"left": {
"Var": "context"
},
"attr": "tls_version"
}
},
"right": {
"Value": "1.3"
}
}
}
}
]
}
},
"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" : "John" }
}
}
]
});
let policyset = PolicySet::from_json_value(value).unwrap();
assert_eq!(policyset.policies().count(), 2);
assert_eq!(policyset.templates().count(), 1);
assert!(policyset.template(&PolicyId::new("template")).is_some());
let link = policyset.policy(&PolicyId::new("link")).unwrap();
assert_eq!(link.template_id(), Some(&PolicyId::new("template")));
assert_eq!(
link.template_links(),
Some(HashMap::from_iter([(
SlotId::principal(),
r#"User::"John""#.parse().unwrap()
)]))
);
if let Err(_) = policyset
.get_linked_policies(PolicyId::new("template"))
.unwrap()
.exactly_one()
{
panic!("Should have exactly one");
};
}
#[test]
fn test_est_policyset_decoding_templates_bad_link_name() {
let value = serde_json::json!({
"staticPolicies": {
"policy1": {
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "12UA45" }
},
"action": {
"op": "==",
"entity": { "type": "Action", "id": "view" }
},
"resource": {
"op": "in",
"entity": { "type": "Folder", "id": "abc" }
},
"conditions": [
{
"kind": "when",
"body": {
"==": {
"left": {
".": {
"left": {
"Var": "context"
},
"attr": "tls_version"
}
},
"right": {
"Value": "1.3"
}
}
}
}
]
}
},
"templates": {
"template1": {
"effect" : "permit",
"principal" : {
"op" : "==",
"slot" : "?principal"
},
"action" : {
"op" : "All"
},
"resource" : {
"op" : "All",
},
"conditions": []
}
},
"templateLinks" : [
{
"newId" : "link",
"templateId" : "non_existent",
"values" : {
"?principal" : { "type" : "User", "id" : "John" }
}
}
]
});
let err = PolicySet::from_json_value(value).err().unwrap();
assert_eq!(
err.to_string(),
"Error deserializing a policy/template from JSON: Error linking policy set: failed to find a template with id `non_existent`"
);
}
#[test]
fn test_est_policyset_decoding_templates_empty_env() {
let value = serde_json::json!({
"staticPolicies": {
"policy1": {
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "12UA45" }
},
"action": {
"op": "==",
"entity": { "type": "Action", "id": "view" }
},
"resource": {
"op": "in",
"entity": { "type": "Folder", "id": "abc" }
},
"conditions": [
{
"kind": "when",
"body": {
"==": {
"left": {
".": {
"left": {
"Var": "context"
},
"attr": "tls_version"
}
},
"right": {
"Value": "1.3"
}
}
}
}
]
}
},
"templates": {
"template1": {
"effect" : "permit",
"principal" : {
"op" : "==",
"slot" : "?principal"
},
"action" : {
"op" : "All"
},
"resource" : {
"op" : "All",
},
"conditions": []
}
},
"templateLinks" : [
{
"newId" : "link",
"templateId" : "template1",
"values" : {},
}
]
});
let err = PolicySet::from_json_value(value).err().unwrap();
assert_eq!(
err.to_string(),
"Error deserializing a policy/template from JSON: Error linking policy set: the following slots were not provided as arguments: ?principal"
);
}
#[test]
fn test_est_policyset_decoding_templates_bad_dup_links() {
let value = serde_json::json!({
"staticPolicies" : {},
"templates": {
"template1": {
"effect" : "permit",
"principal" : {
"op" : "==",
"slot" : "?principal"
},
"action" : {
"op" : "All"
},
"resource" : {
"op" : "All",
},
"conditions": []
}
},
"templateLinks" : [
{
"newId" : "link",
"templateId" : "template1",
"values" : {
"?principal" : { "type" : "User", "id" : "John" },
}
},
{
"newId" : "link",
"templateId" : "template1",
"values" : {
"?principal" : { "type" : "User", "id" : "John" },
}
}
]
});
let err = PolicySet::from_json_value(value).err().unwrap().to_string();
assert_eq!(err, "Error deserializing a policy/template from JSON: Error linking policy set: template-linked policy id `link` conflicts with an existing policy id");
}
#[test]
fn test_est_policyset_decoding_templates_bad_extra_vals() {
let value = serde_json::json!({
"staticPolicies": {
"policy1": {
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "12UA45" }
},
"action": {
"op": "==",
"entity": { "type": "Action", "id": "view" }
},
"resource": {
"op": "in",
"entity": { "type": "Folder", "id": "abc" }
},
"conditions": [
{
"kind": "when",
"body": {
"==": {
"left": {
".": {
"left": {
"Var": "context"
},
"attr": "tls_version"
}
},
"right": {
"Value": "1.3"
}
}
}
}
]
}
},
"templates": {
"template1": {
"effect" : "permit",
"principal" : {
"op" : "==",
"slot" : "?principal"
},
"action" : {
"op" : "All"
},
"resource" : {
"op" : "All",
},
"conditions": []
}
},
"templateLinks" : [
{
"newId" : "link",
"templateId" : "template1",
"values" : {
"?principal" : { "type" : "User", "id" : "John" },
"?resource" : { "type" : "Box", "id" : "ABC" }
}
}
]}
);
let err = PolicySet::from_json_value(value).err().unwrap();
assert_eq!(
err.to_string(),
"Error deserializing a policy/template from JSON: Error linking policy set: the following slots were provided as arguments, but did not exist in the template: ?resource"
);
}
#[test]
fn test_est_policyset_decoding_templates_bad_dup_vals() {
let value = r#" {
"staticPolicies": {
"policy1": {
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "12UA45" }
},
"action": {
"op": "==",
"entity": { "type": "Action", "id": "view" }
},
"resource": {
"op": "in",
"entity": { "type": "Folder", "id": "abc" }
},
"conditions": [
{
"kind": "when",
"body": {
"==": {
"left": {
".": {
"left": {
"Var": "context"
},
"attr": "tls_version"
}
},
"right": {
"Value": "1.3"
}
}
}
}
]
}
},
"templates" : {
"template1": {
"effect" : "permit",
"principal" : {
"op" : "==",
"slot" : "?principal"
},
"action" : {
"op" : "All"
},
"resource" : {
"op" : "All"
},
"conditions": []
}
},
"templateLinks" : [
{
"newId" : "link",
"templateId" : "template1",
"values" : {
"?principal" : { "type" : "User", "id" : "John" },
"?principal" : { "type" : "User", "id" : "Duplicate" }
}
}
]}"#;
let err = PolicySet::from_json_str(value).err().unwrap().to_string();
assert!(err.contains("found duplicate key"));
}
#[test]
fn test_est_policyset_decoding_templates_bad_euid() {
let value = r#" {
"staticPolicies": {
"policy1": {
"effect": "permit",
"principal": {
"op": "==",
"entity": { "type": "User", "id": "12UA45" }
},
"action": {
"op": "==",
"entity": { "type": "Action", "id": "view" }
},
"resource": {
"op": "in",
"entity": { "type": "Folder", "id": "abc" }
},
"conditions": [
{
"kind": "when",
"body": {
"==": {
"left": {
".": {
"left": {
"Var": "context"
},
"attr": "tls_version"
}
},
"right": {
"Value": "1.3"
}
}
}
}
]
}
},
"templates" : {
"template1": {
"effect" : "permit",
"principal" : {
"op" : "==",
"slot" : "?principal"
},
"action" : {
"op" : "All"
},
"resource" : {
"op" : "All"
},
"conditions": []
}
},
"templateLinks" : [
{
"newId" : "link",
"templateId" : "template1",
"values" : {
"?principal" : { "type" : "User" }
}
}
]}"#;
let err = PolicySet::from_json_str(value).err().unwrap().to_string();
assert!(err.contains("while parsing a template link, expected a literal entity reference"));
}
}
#[cfg(test)]
mod test_access {
use super::*;
fn schema() -> Schema {
let src = r#"
type Task = {
"id": Long,
"name": String,
"state": String,
};
type Tasks = Set<Task>;
entity List in [Application] = {
"editors": Team,
"name": String,
"owner": User,
"readers": Team,
"tasks": Tasks,
};
entity Application;
entity User in [Team, Application] = {
"joblevel": Long,
"location": String,
};
entity CoolList;
entity Team in [Team, Application];
action Read, Write, Create;
action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
principal: [User],
resource : [List]
};
action GetList in Read appliesTo {
principal : [User],
resource : [List, CoolList]
};
action GetLists in Read appliesTo {
principal : [User],
resource : [Application]
};
action CreateList in Create appliesTo {
principal : [User],
resource : [Application]
};
"#;
Schema::from_cedarschema_str(src).unwrap().0
}
#[test]
fn principals() {
let schema = schema();
let principals = schema.principals().collect::<HashSet<_>>();
assert_eq!(principals.len(), 1);
let user: EntityTypeName = "User".parse().unwrap();
assert!(principals.contains(&user));
let principals = schema.principals().collect::<Vec<_>>();
assert!(principals.len() > 1);
assert!(principals.iter().all(|ety| **ety == user));
}
#[test]
fn empty_schema_principals_and_resources() {
let empty: Schema = Schema::from_cedarschema_str("").unwrap().0;
assert!(empty.principals().collect::<Vec<_>>().is_empty());
assert!(empty.resources().collect::<Vec<_>>().is_empty());
}
#[test]
fn resources() {
let schema = schema();
let resources = schema.resources().cloned().collect::<HashSet<_>>();
let expected: HashSet<EntityTypeName> = HashSet::from([
"List".parse().unwrap(),
"Application".parse().unwrap(),
"CoolList".parse().unwrap(),
]);
assert_eq!(resources, expected);
}
#[test]
fn principals_for_action() {
let schema = schema();
let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
let got = schema
.principals_for_action(&delete_list)
.unwrap()
.cloned()
.collect::<Vec<_>>();
assert_eq!(got, vec!["User".parse().unwrap()]);
assert!(schema.principals_for_action(&delete_user).is_none());
}
#[test]
fn resources_for_action() {
let schema = schema();
let delete_list: EntityUid = r#"Action::"DeleteList""#.parse().unwrap();
let delete_user: EntityUid = r#"Action::"DeleteUser""#.parse().unwrap();
let create_list: EntityUid = r#"Action::"CreateList""#.parse().unwrap();
let get_list: EntityUid = r#"Action::"GetList""#.parse().unwrap();
let got = schema
.resources_for_action(&delete_list)
.unwrap()
.cloned()
.collect::<Vec<_>>();
assert_eq!(got, vec!["List".parse().unwrap()]);
let got = schema
.resources_for_action(&create_list)
.unwrap()
.cloned()
.collect::<Vec<_>>();
assert_eq!(got, vec!["Application".parse().unwrap()]);
let got = schema
.resources_for_action(&get_list)
.unwrap()
.cloned()
.collect::<HashSet<_>>();
assert_eq!(
got,
HashSet::from(["List".parse().unwrap(), "CoolList".parse().unwrap()])
);
assert!(schema.principals_for_action(&delete_user).is_none());
}
#[test]
fn principal_parents() {
let schema = schema();
let user: EntityTypeName = "User".parse().unwrap();
let parents = schema
.ancestors(&user)
.unwrap()
.cloned()
.collect::<HashSet<_>>();
let expected = HashSet::from(["Team".parse().unwrap(), "Application".parse().unwrap()]);
assert_eq!(parents, expected);
let parents = schema
.ancestors(&"List".parse().unwrap())
.unwrap()
.cloned()
.collect::<HashSet<_>>();
let expected = HashSet::from(["Application".parse().unwrap()]);
assert_eq!(parents, expected);
assert!(schema.ancestors(&"Foo".parse().unwrap()).is_none());
let parents = schema
.ancestors(&"CoolList".parse().unwrap())
.unwrap()
.cloned()
.collect::<HashSet<_>>();
let expected = HashSet::from([]);
assert_eq!(parents, expected);
}
#[test]
fn action_groups() {
let schema = schema();
let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
let expected = ["Read", "Write", "Create"]
.into_iter()
.map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
.collect::<HashSet<EntityUid>>();
assert_eq!(groups, expected);
}
#[test]
fn actions() {
let schema = schema();
let actions = schema.actions().cloned().collect::<HashSet<_>>();
let expected = [
"Read",
"Write",
"Create",
"DeleteList",
"EditShare",
"UpdateList",
"CreateTask",
"UpdateTask",
"DeleteTask",
"GetList",
"GetLists",
"CreateList",
]
.into_iter()
.map(|ty| format!("Action::\"{ty}\"").parse().unwrap())
.collect::<HashSet<EntityUid>>();
assert_eq!(actions, expected);
}
#[test]
fn entities() {
let schema = schema();
let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
let expected = ["List", "Application", "User", "CoolList", "Team"]
.into_iter()
.map(|ty| ty.parse().unwrap())
.collect::<HashSet<EntityTypeName>>();
assert_eq!(entities, expected);
}
}
#[cfg(test)]
mod test_access_namespace {
use super::*;
fn schema() -> Schema {
let src = r#"
namespace Foo {
type Task = {
"id": Long,
"name": String,
"state": String,
};
type Tasks = Set<Task>;
entity List in [Application] = {
"editors": Team,
"name": String,
"owner": User,
"readers": Team,
"tasks": Tasks,
};
entity Application;
entity User in [Team, Application] = {
"joblevel": Long,
"location": String,
};
entity CoolList;
entity Team in [Team, Application];
action Read, Write, Create;
action DeleteList, EditShare, UpdateList, CreateTask, UpdateTask, DeleteTask in Write appliesTo {
principal: [User],
resource : [List]
};
action GetList in Read appliesTo {
principal : [User],
resource : [List, CoolList]
};
action GetLists in Read appliesTo {
principal : [User],
resource : [Application]
};
action CreateList in Create appliesTo {
principal : [User],
resource : [Application]
};
}
"#;
Schema::from_cedarschema_str(src).unwrap().0
}
#[test]
fn principals() {
let schema = schema();
let principals = schema.principals().collect::<HashSet<_>>();
assert_eq!(principals.len(), 1);
let user: EntityTypeName = "Foo::User".parse().unwrap();
assert!(principals.contains(&user));
let principals = schema.principals().collect::<Vec<_>>();
assert!(principals.len() > 1);
assert!(principals.iter().all(|ety| **ety == user));
}
#[test]
fn empty_schema_principals_and_resources() {
let empty: Schema = Schema::from_cedarschema_str("").unwrap().0;
assert!(empty.principals().collect::<Vec<_>>().is_empty());
assert!(empty.resources().collect::<Vec<_>>().is_empty());
}
#[test]
fn resources() {
let schema = schema();
let resources = schema.resources().cloned().collect::<HashSet<_>>();
let expected: HashSet<EntityTypeName> = HashSet::from([
"Foo::List".parse().unwrap(),
"Foo::Application".parse().unwrap(),
"Foo::CoolList".parse().unwrap(),
]);
assert_eq!(resources, expected);
}
#[test]
fn principals_for_action() {
let schema = schema();
let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
let got = schema
.principals_for_action(&delete_list)
.unwrap()
.cloned()
.collect::<Vec<_>>();
assert_eq!(got, vec!["Foo::User".parse().unwrap()]);
assert!(schema.principals_for_action(&delete_user).is_none());
}
#[test]
fn resources_for_action() {
let schema = schema();
let delete_list: EntityUid = r#"Foo::Action::"DeleteList""#.parse().unwrap();
let delete_user: EntityUid = r#"Foo::Action::"DeleteUser""#.parse().unwrap();
let create_list: EntityUid = r#"Foo::Action::"CreateList""#.parse().unwrap();
let get_list: EntityUid = r#"Foo::Action::"GetList""#.parse().unwrap();
let got = schema
.resources_for_action(&delete_list)
.unwrap()
.cloned()
.collect::<Vec<_>>();
assert_eq!(got, vec!["Foo::List".parse().unwrap()]);
let got = schema
.resources_for_action(&create_list)
.unwrap()
.cloned()
.collect::<Vec<_>>();
assert_eq!(got, vec!["Foo::Application".parse().unwrap()]);
let got = schema
.resources_for_action(&get_list)
.unwrap()
.cloned()
.collect::<HashSet<_>>();
assert_eq!(
got,
HashSet::from([
"Foo::List".parse().unwrap(),
"Foo::CoolList".parse().unwrap()
])
);
assert!(schema.principals_for_action(&delete_user).is_none());
}
#[test]
fn principal_parents() {
let schema = schema();
let user: EntityTypeName = "Foo::User".parse().unwrap();
let parents = schema
.ancestors(&user)
.unwrap()
.cloned()
.collect::<HashSet<_>>();
let expected = HashSet::from([
"Foo::Team".parse().unwrap(),
"Foo::Application".parse().unwrap(),
]);
assert_eq!(parents, expected);
let parents = schema
.ancestors(&"Foo::List".parse().unwrap())
.unwrap()
.cloned()
.collect::<HashSet<_>>();
let expected = HashSet::from(["Foo::Application".parse().unwrap()]);
assert_eq!(parents, expected);
assert!(schema.ancestors(&"Foo::Foo".parse().unwrap()).is_none());
let parents = schema
.ancestors(&"Foo::CoolList".parse().unwrap())
.unwrap()
.cloned()
.collect::<HashSet<_>>();
let expected = HashSet::from([]);
assert_eq!(parents, expected);
}
#[test]
fn action_groups() {
let schema = schema();
let groups = schema.action_groups().cloned().collect::<HashSet<_>>();
let expected = ["Read", "Write", "Create"]
.into_iter()
.map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
.collect::<HashSet<EntityUid>>();
assert_eq!(groups, expected);
}
#[test]
fn actions() {
let schema = schema();
let actions = schema.actions().cloned().collect::<HashSet<_>>();
let expected = [
"Read",
"Write",
"Create",
"DeleteList",
"EditShare",
"UpdateList",
"CreateTask",
"UpdateTask",
"DeleteTask",
"GetList",
"GetLists",
"CreateList",
]
.into_iter()
.map(|ty| format!("Foo::Action::\"{ty}\"").parse().unwrap())
.collect::<HashSet<EntityUid>>();
assert_eq!(actions, expected);
}
#[test]
fn entities() {
let schema = schema();
let entities = schema.entity_types().cloned().collect::<HashSet<_>>();
let expected = [
"Foo::List",
"Foo::Application",
"Foo::User",
"Foo::CoolList",
"Foo::Team",
]
.into_iter()
.map(|ty| ty.parse().unwrap())
.collect::<HashSet<EntityTypeName>>();
assert_eq!(entities, expected);
}
}