use crate::ast::*;
use crate::parser::Loc;
use annotation::{Annotation, Annotations};
use educe::Educe;
use itertools::Itertools;
use linked_hash_map::LinkedHashMap;
use miette::Diagnostic;
use nonempty::{nonempty, NonEmpty};
use serde::{Deserialize, Serialize};
use smol_str::SmolStr;
use std::{
collections::{HashMap, HashSet},
str::FromStr,
sync::Arc,
};
use thiserror::Error;
#[cfg(feature = "wasm")]
extern crate tsify;
macro_rules! cfg_tolerant_ast {
($($item:item)*) => {
$(
#[cfg(feature = "tolerant-ast")]
$item
)*
};
}
cfg_tolerant_ast! {
use super::expr_allows_errors::AstExprErrorKind;
use crate::ast::expr_allows_errors::ExprWithErrsBuilder;
use crate::expr_builder::ExprBuilder;
use crate::parser::err::ParseErrors;
use crate::parser::err::ToASTError;
use crate::parser::err::ToASTErrorKind;
static DEFAULT_ANNOTATIONS: std::sync::LazyLock<Arc<Annotations>> =
std::sync::LazyLock::new(|| Arc::new(Annotations::default()));
static DEFAULT_PRINCIPAL_CONSTRAINT: std::sync::LazyLock<PrincipalConstraint> =
std::sync::LazyLock::new(PrincipalConstraint::any);
static DEFAULT_RESOURCE_CONSTRAINT: std::sync::LazyLock<ResourceConstraint> =
std::sync::LazyLock::new(ResourceConstraint::any);
static DEFAULT_ACTION_CONSTRAINT: std::sync::LazyLock<ActionConstraint> =
std::sync::LazyLock::new(ActionConstraint::any);
static DEFAULT_ERROR_EXPR: std::sync::LazyLock<Arc<Expr>> = std::sync::LazyLock::new(|| {
Arc::new(
<ExprWithErrsBuilder as ExprBuilder>::new()
.error(ParseErrors::singleton(ToASTError::new(
ToASTErrorKind::ASTErrorNode,
Some(Loc::new(0..1, "ASTErrorNode".into())),
)))
.unwrap_infallible(),
)
});
}
#[derive(Clone, Hash, Eq, PartialEq, Debug)]
pub struct Template {
body: TemplateBody,
slots: Vec<Slot>,
}
impl From<Template> for TemplateBody {
fn from(val: Template) -> Self {
val.body
}
}
impl Template {
pub fn check_invariant(&self) {
#[cfg(debug_assertions)]
{
for slot in self.body.condition().slots() {
assert!(self.slots.contains(&slot));
}
for slot in self.slots() {
assert!(self.body.condition().slots().contains(slot));
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "policies just have this many components"
)]
pub fn new(
id: PolicyID,
loc: Option<Loc>,
annotations: Annotations,
effect: Effect,
principal_constraint: PrincipalConstraint,
action_constraint: ActionConstraint,
resource_constraint: ResourceConstraint,
non_scope_constraint: Option<Expr>,
) -> Self {
let body = TemplateBody::new(
id,
loc,
annotations,
effect,
principal_constraint,
action_constraint,
resource_constraint,
non_scope_constraint,
);
Template::from(body)
}
#[cfg(feature = "tolerant-ast")]
pub fn error(id: PolicyID, loc: Option<Loc>) -> Self {
let body = TemplateBody::error(id, loc);
Template::from(body)
}
#[expect(
clippy::too_many_arguments,
reason = "policies just have this many components"
)]
pub fn new_shared(
id: PolicyID,
loc: Option<Loc>,
annotations: Arc<Annotations>,
effect: Effect,
principal_constraint: PrincipalConstraint,
action_constraint: ActionConstraint,
resource_constraint: ResourceConstraint,
non_scope_constraint: Option<Arc<Expr>>,
) -> Self {
let body = TemplateBody::new_shared(
id,
loc,
annotations,
effect,
principal_constraint,
action_constraint,
resource_constraint,
non_scope_constraint,
);
Template::from(body)
}
#[expect(clippy::type_complexity, reason = "policies just have many components")]
pub(crate) fn into_template_components_opt(
self,
) -> Option<(
PolicyID,
Arc<Annotations>,
Effect,
PrincipalConstraint,
ActionConstraint,
ResourceConstraint,
Option<Arc<Expr>>,
)> {
self.body.into_components_opt()
}
pub fn principal_constraint(&self) -> &PrincipalConstraint {
self.body.principal_constraint()
}
pub fn action_constraint(&self) -> &ActionConstraint {
self.body.action_constraint()
}
pub fn resource_constraint(&self) -> &ResourceConstraint {
self.body.resource_constraint()
}
pub fn non_scope_constraints(&self) -> Option<&Expr> {
self.body.non_scope_constraints()
}
pub fn non_scope_constraints_arc(&self) -> Option<&Arc<Expr>> {
self.body.non_scope_constraints_arc()
}
pub fn id(&self) -> &PolicyID {
self.body.id()
}
pub fn new_id(&self, id: PolicyID) -> Self {
Template {
body: self.body.new_id(id),
slots: self.slots.clone(),
}
}
pub fn loc(&self) -> Option<&Loc> {
self.body.loc()
}
pub fn effect(&self) -> Effect {
self.body.effect()
}
pub fn annotation(&self, key: &AnyId) -> Option<&Annotation> {
self.body.annotation(key)
}
pub fn annotations(&self) -> impl Iterator<Item = (&AnyId, &Annotation)> {
self.body.annotations()
}
pub fn annotations_arc(&self) -> &Arc<Annotations> {
self.body.annotations_arc()
}
pub fn condition(&self) -> Expr {
self.body.condition()
}
pub fn slots(&self) -> impl Iterator<Item = &Slot> {
self.slots.iter()
}
pub fn is_static(&self) -> bool {
self.slots.is_empty()
}
pub fn check_binding(
template: &Template,
values: &HashMap<SlotId, EntityUID>,
) -> Result<(), LinkingError> {
let unbound = template
.slots
.iter()
.filter(|slot| !values.contains_key(&slot.id))
.collect::<Vec<_>>();
let extra = values
.iter()
.filter_map(|(slot, _)| {
if !template
.slots
.iter()
.any(|template_slot| template_slot.id == *slot)
{
Some(slot)
} else {
None
}
})
.collect::<Vec<_>>();
if unbound.is_empty() && extra.is_empty() {
Ok(())
} else {
Err(LinkingError::from_unbound_and_extras(
unbound.into_iter().map(|slot| slot.id),
extra.into_iter().copied(),
))
}
}
pub fn link(
template: Arc<Template>,
new_id: PolicyID,
values: HashMap<SlotId, EntityUID>,
) -> Result<Policy, LinkingError> {
Template::check_binding(&template, &values)
.map(|_| Policy::new(template, Some(new_id), values))
}
pub fn link_static_policy(p: StaticPolicy) -> (Arc<Template>, Policy) {
let body: TemplateBody = p.into();
let t = Arc::new(Self {
body,
slots: vec![],
});
t.check_invariant();
let p = Policy::new(Arc::clone(&t), None, HashMap::new());
(t, p)
}
}
impl From<TemplateBody> for Template {
fn from(body: TemplateBody) -> Self {
let slots = body.condition().slots().collect::<Vec<_>>();
Self { body, slots }
}
}
impl std::fmt::Display for Template {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.body)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Diagnostic, Error)]
pub enum LinkingError {
#[error(fmt = describe_arity_error)]
ArityError {
unbound_values: Vec<SlotId>,
extra_values: Vec<SlotId>,
},
#[error("failed to find a template with id `{id}`")]
NoSuchTemplate {
id: PolicyID,
},
#[error("template-linked policy id `{id}` conflicts with an existing policy id")]
PolicyIdConflict {
id: PolicyID,
},
}
impl LinkingError {
fn from_unbound_and_extras(
unbound: impl Iterator<Item = SlotId>,
extra: impl Iterator<Item = SlotId>,
) -> Self {
Self::ArityError {
unbound_values: unbound.collect(),
extra_values: extra.collect(),
}
}
}
fn describe_arity_error(
unbound_values: &[SlotId],
extra_values: &[SlotId],
fmt: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
match (unbound_values.len(), extra_values.len()) {
#[expect(clippy::unreachable, reason = "0,0 case is not an error")]
(0,0) => unreachable!(),
(_unbound, 0) => write!(fmt, "the following slots were not provided as arguments: {}", unbound_values.iter().join(",")),
(0, _extra) => write!(fmt, "the following slots were provided as arguments, but did not exist in the template: {}", extra_values.iter().join(",")),
(_unbound, _extra) => write!(fmt, "the following slots were not provided as arguments: {}. The following slots were provided as arguments, but did not exist in the template: {}", unbound_values.iter().join(","), extra_values.iter().join(",")),
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Policy {
template: Arc<Template>,
link: Option<PolicyID>,
values: HashMap<SlotId, EntityUID>,
}
impl Policy {
pub(crate) fn new(template: Arc<Template>, link_id: Option<PolicyID>, values: SlotEnv) -> Self {
#[cfg(debug_assertions)]
{
#[expect(
clippy::expect_used,
reason = "asserts (value total map invariant) which is justified at call sites"
)]
Template::check_binding(&template, &values).expect("(values total map) does not hold!");
}
Self {
template,
link: link_id,
values,
}
}
pub fn from_when_clause(effect: Effect, when: Expr, id: PolicyID, loc: Option<Loc>) -> Self {
Self::from_when_clause_annos(
effect,
Arc::new(when),
id,
loc,
Arc::new(Annotations::default()),
)
}
pub fn from_when_clause_annos(
effect: Effect,
when: Arc<Expr>,
id: PolicyID,
loc: Option<Loc>,
annotations: Arc<Annotations>,
) -> Self {
let t = Template::new_shared(
id,
loc,
annotations,
effect,
PrincipalConstraint::any(),
ActionConstraint::any(),
ResourceConstraint::any(),
Some(when),
);
Self::new(Arc::new(t), None, SlotEnv::new())
}
pub(crate) fn into_components(self) -> (Arc<Template>, Option<PolicyID>, SlotEnv) {
(self.template, self.link, self.values)
}
pub fn template(&self) -> &Template {
&self.template
}
pub(crate) fn template_arc(&self) -> Arc<Template> {
Arc::clone(&self.template)
}
pub fn effect(&self) -> Effect {
self.template.effect()
}
pub fn annotation(&self, key: &AnyId) -> Option<&Annotation> {
self.template.annotation(key)
}
pub fn annotations(&self) -> impl Iterator<Item = (&AnyId, &Annotation)> {
self.template.annotations()
}
pub fn annotations_arc(&self) -> &Arc<Annotations> {
self.template.annotations_arc()
}
pub fn principal_constraint(&self) -> PrincipalConstraint {
let constraint = self.template.principal_constraint().clone();
match self.values.get(&SlotId::principal()) {
None => constraint,
Some(principal) => constraint.with_filled_slot(Arc::new(principal.clone())),
}
}
pub fn action_constraint(&self) -> &ActionConstraint {
self.template.action_constraint()
}
pub fn resource_constraint(&self) -> ResourceConstraint {
let constraint = self.template.resource_constraint().clone();
match self.values.get(&SlotId::resource()) {
None => constraint,
Some(resource) => constraint.with_filled_slot(Arc::new(resource.clone())),
}
}
pub fn non_scope_constraints(&self) -> Option<&Expr> {
self.template.non_scope_constraints()
}
pub fn non_scope_constraints_arc(&self) -> Option<&Arc<Expr>> {
self.template.non_scope_constraints_arc()
}
pub fn condition(&self) -> Expr {
self.template.condition()
}
pub fn env(&self) -> &SlotEnv {
&self.values
}
pub fn id(&self) -> &PolicyID {
self.link.as_ref().unwrap_or_else(|| self.template.id())
}
pub fn new_id(&self, id: PolicyID) -> Self {
match self.link {
None => Policy {
template: Arc::new(self.template.new_id(id)),
link: None,
values: self.values.clone(),
},
Some(_) => Policy {
template: self.template.clone(),
link: Some(id),
values: self.values.clone(),
},
}
}
pub(crate) fn new_template_id(self, id: PolicyID) -> Option<Self> {
self.link.map(|link| Policy {
template: Arc::new(self.template.new_id(id)),
link: Some(link),
values: self.values,
})
}
pub fn loc(&self) -> Option<&Loc> {
self.template.loc()
}
pub fn is_static(&self) -> bool {
self.link.is_none()
}
pub fn unknown_entities(&self) -> HashSet<EntityUID> {
self.condition()
.unknowns()
.filter_map(
|Unknown {
name,
type_annotation,
}| {
if matches!(type_annotation, Some(Type::Entity { .. })) {
EntityUID::from_str(name.as_str()).ok()
} else {
None
}
},
)
.collect()
}
}
impl std::fmt::Display for Policy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_static() {
write!(f, "{}", self.template())
} else {
write!(
f,
"Template Instance of {}, slots: [{}]",
self.template().id(),
display_slot_env(self.env())
)
}
}
}
pub type SlotEnv = HashMap<SlotId, EntityUID>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LiteralPolicy {
template_id: PolicyID,
link_id: Option<PolicyID>,
values: SlotEnv,
}
impl LiteralPolicy {
pub fn static_policy(template_id: PolicyID) -> Self {
Self {
template_id,
link_id: None,
values: SlotEnv::new(),
}
}
pub fn template_linked_policy(
template_id: PolicyID,
link_id: PolicyID,
values: SlotEnv,
) -> Self {
Self {
template_id,
link_id: Some(link_id),
values,
}
}
pub fn value(&self, slot: &SlotId) -> Option<&EntityUID> {
self.values.get(slot)
}
}
impl std::hash::Hash for LiteralPolicy {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.template_id.hash(state);
let mut buf = self.values.iter().collect::<Vec<_>>();
buf.sort();
for (id, euid) in buf {
id.hash(state);
euid.hash(state);
}
}
}
#[cfg(test)]
mod hashing_tests {
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
use super::*;
fn compute_hash(ir: &LiteralPolicy) -> u64 {
let mut s = DefaultHasher::new();
ir.hash(&mut s);
s.finish()
}
fn build_template_linked_policy() -> LiteralPolicy {
let mut map = HashMap::new();
map.insert(SlotId::principal(), EntityUID::with_eid("eid"));
LiteralPolicy {
template_id: PolicyID::from_string("template"),
link_id: Some(PolicyID::from_string("id")),
values: map,
}
}
#[test]
fn hash_property_instances() {
let a = build_template_linked_policy();
let b = build_template_linked_policy();
assert_eq!(a, b);
assert_eq!(compute_hash(&a), compute_hash(&b));
}
}
#[derive(Debug, Diagnostic, Error)]
pub enum ReificationError {
#[error("the id linked to does not exist")]
NoSuchTemplate(PolicyID),
#[error(transparent)]
#[diagnostic(transparent)]
Linking(#[from] LinkingError),
}
impl LiteralPolicy {
pub fn reify(
self,
templates: &LinkedHashMap<PolicyID, Arc<Template>>,
) -> Result<Policy, ReificationError> {
let template = templates
.get(&self.template_id)
.ok_or_else(|| ReificationError::NoSuchTemplate(self.template_id().clone()))?;
Template::check_binding(template, &self.values).map_err(ReificationError::Linking)?;
Ok(Policy::new(template.clone(), self.link_id, self.values))
}
pub fn get(&self, id: &SlotId) -> Option<&EntityUID> {
self.values.get(id)
}
pub fn id(&self) -> &PolicyID {
self.link_id.as_ref().unwrap_or(&self.template_id)
}
pub fn template_id(&self) -> &PolicyID {
&self.template_id
}
pub fn is_static(&self) -> bool {
self.link_id.is_none()
}
}
fn display_slot_env(env: &SlotEnv) -> String {
env.iter()
.map(|(slot, value)| format!("{slot} -> {value}"))
.join(",")
}
impl std::fmt::Display for LiteralPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.is_static() {
write!(f, "Static policy w/ ID {}", self.template_id())
} else {
write!(
f,
"Template linked policy of {}, slots: [{}]",
self.template_id(),
display_slot_env(&self.values),
)
}
}
}
impl From<Policy> for LiteralPolicy {
fn from(p: Policy) -> Self {
Self {
template_id: p.template.id().clone(),
link_id: p.link,
values: p.values,
}
}
}
#[derive(Clone, Hash, Eq, PartialEq, Debug)]
pub struct StaticPolicy(TemplateBody);
impl StaticPolicy {
pub fn id(&self) -> &PolicyID {
self.0.id()
}
pub fn new_id(&self, id: PolicyID) -> Self {
StaticPolicy(self.0.new_id(id))
}
pub fn loc(&self) -> Option<&Loc> {
self.0.loc()
}
pub fn effect(&self) -> Effect {
self.0.effect()
}
pub fn annotation(&self, key: &AnyId) -> Option<&Annotation> {
self.0.annotation(key)
}
pub fn annotations(&self) -> impl Iterator<Item = (&AnyId, &Annotation)> {
self.0.annotations()
}
pub fn principal_constraint(&self) -> &PrincipalConstraint {
self.0.principal_constraint()
}
pub fn principal_constraint_expr(&self) -> Expr {
self.0.principal_constraint_expr()
}
pub fn action_constraint(&self) -> &ActionConstraint {
self.0.action_constraint()
}
pub fn action_constraint_expr(&self) -> Expr {
self.0.action_constraint_expr()
}
pub fn resource_constraint(&self) -> &ResourceConstraint {
self.0.resource_constraint()
}
pub fn resource_constraint_expr(&self) -> Expr {
self.0.resource_constraint_expr()
}
pub fn non_scope_constraints(&self) -> Option<&Expr> {
self.0.non_scope_constraints()
}
pub fn condition(&self) -> Expr {
self.0.condition()
}
#[expect(
clippy::too_many_arguments,
reason = "policies just have this many components"
)]
pub fn new(
id: PolicyID,
loc: Option<Loc>,
annotations: Annotations,
effect: Effect,
principal_constraint: PrincipalConstraint,
action_constraint: ActionConstraint,
resource_constraint: ResourceConstraint,
non_scope_constraints: Option<Expr>,
) -> Result<Self, UnexpectedSlotError> {
let body = TemplateBody::new(
id,
loc,
annotations,
effect,
principal_constraint,
action_constraint,
resource_constraint,
non_scope_constraints,
);
let first_slot = body.condition().slots().next();
match first_slot {
Some(slot) => Err(UnexpectedSlotError::FoundSlot(slot))?,
None => Ok(Self(body)),
}
}
}
impl TryFrom<Template> for StaticPolicy {
type Error = UnexpectedSlotError;
fn try_from(value: Template) -> Result<Self, Self::Error> {
let o = value.slots().next();
match o {
Some(slot_id) => Err(Self::Error::FoundSlot(slot_id.clone())),
None => Ok(Self(value.body)),
}
}
}
impl From<StaticPolicy> for Policy {
fn from(p: StaticPolicy) -> Policy {
let (_, policy) = Template::link_static_policy(p);
policy
}
}
impl From<StaticPolicy> for Arc<Template> {
fn from(p: StaticPolicy) -> Self {
let (t, _) = Template::link_static_policy(p);
t
}
}
#[derive(Educe, Clone, Debug)]
#[educe(PartialEq, Eq, Hash)]
pub struct TemplateBodyImpl {
id: PolicyID,
#[educe(PartialEq(ignore))]
#[educe(Hash(ignore))]
loc: Option<Loc>,
annotations: Arc<Annotations>,
effect: Effect,
principal_constraint: PrincipalConstraint,
action_constraint: ActionConstraint,
resource_constraint: ResourceConstraint,
non_scope_constraints: Option<Arc<Expr>>,
}
#[derive(Clone, Hash, Eq, PartialEq, Debug)]
pub enum TemplateBody {
TemplateBody(TemplateBodyImpl),
#[cfg(feature = "tolerant-ast")]
TemplateBodyError(PolicyID, Option<Loc>),
}
impl TemplateBody {
pub fn id(&self) -> &PolicyID {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl { id, .. }) => id,
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(id, _) => id,
}
}
pub fn loc(&self) -> Option<&Loc> {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl { loc, .. }) => loc.as_ref(),
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, loc) => loc.as_ref(),
}
}
pub fn new_id(&self, id: PolicyID) -> Self {
match self {
TemplateBody::TemplateBody(t) => {
let mut new = t.clone();
new.id = id;
TemplateBody::TemplateBody(new)
}
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, loc) => {
TemplateBody::TemplateBodyError(id, loc.clone())
}
}
}
#[cfg(feature = "tolerant-ast")]
pub fn error(id: PolicyID, loc: Option<Loc>) -> Self {
TemplateBody::TemplateBodyError(id, loc)
}
pub fn effect(&self) -> Effect {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl { effect, .. }) => *effect,
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => Effect::Forbid,
}
}
pub fn annotation(&self, key: &AnyId) -> Option<&Annotation> {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl { annotations, .. }) => {
annotations.get(key)
}
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => None,
}
}
pub fn annotations_arc(&self) -> &Arc<Annotations> {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl { annotations, .. }) => annotations,
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => &DEFAULT_ANNOTATIONS,
}
}
pub fn annotations(&self) -> impl Iterator<Item = (&AnyId, &Annotation)> {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl { annotations, .. }) => annotations.iter(),
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => DEFAULT_ANNOTATIONS.iter(),
}
}
pub fn principal_constraint(&self) -> &PrincipalConstraint {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl {
principal_constraint,
..
}) => principal_constraint,
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => &DEFAULT_PRINCIPAL_CONSTRAINT,
}
}
pub fn principal_constraint_expr(&self) -> Expr {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl {
principal_constraint,
..
}) => principal_constraint.as_expr(),
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => DEFAULT_PRINCIPAL_CONSTRAINT.as_expr(),
}
}
pub fn action_constraint(&self) -> &ActionConstraint {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl {
action_constraint, ..
}) => action_constraint,
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => &DEFAULT_ACTION_CONSTRAINT,
}
}
pub fn action_constraint_expr(&self) -> Expr {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl {
action_constraint, ..
}) => action_constraint.as_expr(),
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => DEFAULT_ACTION_CONSTRAINT.as_expr(),
}
}
pub fn resource_constraint(&self) -> &ResourceConstraint {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl {
resource_constraint,
..
}) => resource_constraint,
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => &DEFAULT_RESOURCE_CONSTRAINT,
}
}
pub fn resource_constraint_expr(&self) -> Expr {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl {
resource_constraint,
..
}) => resource_constraint.as_expr(),
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => DEFAULT_RESOURCE_CONSTRAINT.as_expr(),
}
}
pub fn non_scope_constraints(&self) -> Option<&Expr> {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl {
non_scope_constraints,
..
}) => non_scope_constraints.as_ref().map(|e| e.as_ref()),
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => Some(&DEFAULT_ERROR_EXPR),
}
}
pub fn non_scope_constraints_arc(&self) -> Option<&Arc<Expr>> {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl {
non_scope_constraints,
..
}) => non_scope_constraints.as_ref(),
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => Some(&DEFAULT_ERROR_EXPR),
}
}
#[expect(clippy::type_complexity, reason = "policies just have many components")]
pub(crate) fn into_components_opt(
self,
) -> Option<(
PolicyID,
Arc<Annotations>,
Effect,
PrincipalConstraint,
ActionConstraint,
ResourceConstraint,
Option<Arc<Expr>>,
)> {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl {
id,
loc: _,
annotations,
effect,
principal_constraint,
action_constraint,
resource_constraint,
non_scope_constraints,
}) => Some((
id,
annotations,
effect,
principal_constraint,
action_constraint,
resource_constraint,
non_scope_constraints,
)),
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => None,
}
}
pub fn condition(&self) -> Expr {
match self {
TemplateBody::TemplateBody(TemplateBodyImpl { .. }) => {
let loc = self.loc().cloned();
Expr::and(
self.principal_constraint_expr(),
Expr::and(
self.action_constraint_expr(),
Expr::and(
self.resource_constraint_expr(),
self.non_scope_constraints()
.cloned()
.unwrap_or_else(|| Expr::val(true)),
)
.with_maybe_source_loc(loc.clone()),
)
.with_maybe_source_loc(loc.clone()),
)
.with_maybe_source_loc(loc)
}
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(_, _) => DEFAULT_ERROR_EXPR.as_ref().clone(),
}
}
#[expect(
clippy::too_many_arguments,
reason = "policies just have this many components"
)]
pub fn new_shared(
id: PolicyID,
loc: Option<Loc>,
annotations: Arc<Annotations>,
effect: Effect,
principal_constraint: PrincipalConstraint,
action_constraint: ActionConstraint,
resource_constraint: ResourceConstraint,
non_scope_constraints: Option<Arc<Expr>>,
) -> Self {
Self::TemplateBody(TemplateBodyImpl {
id,
loc,
annotations,
effect,
principal_constraint,
action_constraint,
resource_constraint,
non_scope_constraints,
})
}
#[expect(
clippy::too_many_arguments,
reason = "policies just have this many components"
)]
pub fn new(
id: PolicyID,
loc: Option<Loc>,
annotations: Annotations,
effect: Effect,
principal_constraint: PrincipalConstraint,
action_constraint: ActionConstraint,
resource_constraint: ResourceConstraint,
non_scope_constraints: Option<Expr>,
) -> Self {
Self::TemplateBody(TemplateBodyImpl {
id,
loc,
annotations: Arc::new(annotations),
effect,
principal_constraint,
action_constraint,
resource_constraint,
non_scope_constraints: non_scope_constraints.map(Arc::new),
})
}
}
impl From<StaticPolicy> for TemplateBody {
fn from(p: StaticPolicy) -> Self {
p.0
}
}
impl std::fmt::Display for TemplateBody {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TemplateBody::TemplateBody(template_body_impl) => {
template_body_impl.annotations.fmt(f)?;
write!(
f,
"{}(\n {},\n {},\n {}\n)",
self.effect(),
self.principal_constraint(),
self.action_constraint(),
self.resource_constraint(),
)?;
if let Some(non_scope_constraints) = self.non_scope_constraints() {
write!(f, " when {{\n {non_scope_constraints}\n}};")
} else {
write!(f, ";")
}
}
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(policy_id, _) => {
write!(f, "TemplateBody::TemplateBodyError({policy_id})")
}
}
}
}
#[derive(Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub struct PrincipalConstraint {
pub(crate) constraint: PrincipalOrResourceConstraint,
}
impl PrincipalConstraint {
pub fn new(constraint: PrincipalOrResourceConstraint) -> Self {
PrincipalConstraint { constraint }
}
pub fn as_inner(&self) -> &PrincipalOrResourceConstraint {
&self.constraint
}
pub fn into_inner(self) -> PrincipalOrResourceConstraint {
self.constraint
}
pub fn as_expr(&self) -> Expr {
self.constraint.as_expr(PrincipalOrResource::Principal)
}
pub fn any() -> Self {
PrincipalConstraint {
constraint: PrincipalOrResourceConstraint::any(),
}
}
pub fn is_eq(euid: Arc<EntityUID>) -> Self {
PrincipalConstraint {
constraint: PrincipalOrResourceConstraint::is_eq(euid),
}
}
pub fn is_eq_slot() -> Self {
Self {
constraint: PrincipalOrResourceConstraint::is_eq_slot(),
}
}
pub fn is_in(euid: Arc<EntityUID>) -> Self {
PrincipalConstraint {
constraint: PrincipalOrResourceConstraint::is_in(euid),
}
}
pub fn is_in_slot() -> Self {
Self {
constraint: PrincipalOrResourceConstraint::is_in_slot(),
}
}
pub fn is_entity_type_in_slot(entity_type: Arc<EntityType>) -> Self {
Self {
constraint: PrincipalOrResourceConstraint::is_entity_type_in_slot(entity_type),
}
}
pub fn is_entity_type_in(entity_type: Arc<EntityType>, in_entity: Arc<EntityUID>) -> Self {
Self {
constraint: PrincipalOrResourceConstraint::is_entity_type_in(entity_type, in_entity),
}
}
pub fn is_entity_type(entity_type: Arc<EntityType>) -> Self {
Self {
constraint: PrincipalOrResourceConstraint::is_entity_type(entity_type),
}
}
pub fn with_filled_slot(self, euid: Arc<EntityUID>) -> Self {
match self.constraint {
PrincipalOrResourceConstraint::Eq(EntityReference::Slot(_)) => Self {
constraint: PrincipalOrResourceConstraint::Eq(EntityReference::EUID(euid)),
},
PrincipalOrResourceConstraint::In(EntityReference::Slot(_)) => Self {
constraint: PrincipalOrResourceConstraint::In(EntityReference::EUID(euid)),
},
PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::Slot(_)) => Self {
constraint: PrincipalOrResourceConstraint::IsIn(
entity_type,
EntityReference::EUID(euid),
),
},
PrincipalOrResourceConstraint::Is(_)
| PrincipalOrResourceConstraint::Any
| PrincipalOrResourceConstraint::Eq(EntityReference::EUID(_))
| PrincipalOrResourceConstraint::In(EntityReference::EUID(_))
| PrincipalOrResourceConstraint::IsIn(_, EntityReference::EUID(_)) => self,
}
}
}
impl std::fmt::Display for PrincipalConstraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.constraint.display(PrincipalOrResource::Principal)
)
}
}
#[derive(Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub struct ResourceConstraint {
pub(crate) constraint: PrincipalOrResourceConstraint,
}
impl ResourceConstraint {
pub fn new(constraint: PrincipalOrResourceConstraint) -> Self {
ResourceConstraint { constraint }
}
pub fn as_inner(&self) -> &PrincipalOrResourceConstraint {
&self.constraint
}
pub fn into_inner(self) -> PrincipalOrResourceConstraint {
self.constraint
}
pub fn as_expr(&self) -> Expr {
self.constraint.as_expr(PrincipalOrResource::Resource)
}
pub fn any() -> Self {
ResourceConstraint {
constraint: PrincipalOrResourceConstraint::any(),
}
}
pub fn is_eq(euid: Arc<EntityUID>) -> Self {
ResourceConstraint {
constraint: PrincipalOrResourceConstraint::is_eq(euid),
}
}
pub fn is_eq_slot() -> Self {
Self {
constraint: PrincipalOrResourceConstraint::is_eq_slot(),
}
}
pub fn is_in_slot() -> Self {
Self {
constraint: PrincipalOrResourceConstraint::is_in_slot(),
}
}
pub fn is_in(euid: Arc<EntityUID>) -> Self {
ResourceConstraint {
constraint: PrincipalOrResourceConstraint::is_in(euid),
}
}
pub fn is_entity_type_in_slot(entity_type: Arc<EntityType>) -> Self {
Self {
constraint: PrincipalOrResourceConstraint::is_entity_type_in_slot(entity_type),
}
}
pub fn is_entity_type_in(entity_type: Arc<EntityType>, in_entity: Arc<EntityUID>) -> Self {
Self {
constraint: PrincipalOrResourceConstraint::is_entity_type_in(entity_type, in_entity),
}
}
pub fn is_entity_type(entity_type: Arc<EntityType>) -> Self {
Self {
constraint: PrincipalOrResourceConstraint::is_entity_type(entity_type),
}
}
pub fn with_filled_slot(self, euid: Arc<EntityUID>) -> Self {
match self.constraint {
PrincipalOrResourceConstraint::Eq(EntityReference::Slot(_)) => Self {
constraint: PrincipalOrResourceConstraint::Eq(EntityReference::EUID(euid)),
},
PrincipalOrResourceConstraint::In(EntityReference::Slot(_)) => Self {
constraint: PrincipalOrResourceConstraint::In(EntityReference::EUID(euid)),
},
PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::Slot(_)) => Self {
constraint: PrincipalOrResourceConstraint::IsIn(
entity_type,
EntityReference::EUID(euid),
),
},
PrincipalOrResourceConstraint::Is(_)
| PrincipalOrResourceConstraint::Any
| PrincipalOrResourceConstraint::Eq(EntityReference::EUID(_))
| PrincipalOrResourceConstraint::In(EntityReference::EUID(_))
| PrincipalOrResourceConstraint::IsIn(_, EntityReference::EUID(_)) => self,
}
}
}
impl std::fmt::Display for ResourceConstraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
self.as_inner().display(PrincipalOrResource::Resource)
)
}
}
#[derive(Educe, Clone, Debug, Eq)]
#[educe(Hash, PartialEq, PartialOrd, Ord)]
pub enum EntityReference {
EUID(Arc<EntityUID>),
Slot(
#[educe(PartialEq(ignore))]
#[educe(PartialOrd(ignore))]
#[educe(Hash(ignore))]
Option<Loc>,
),
}
impl EntityReference {
pub fn euid(euid: Arc<EntityUID>) -> Self {
Self::EUID(euid)
}
pub fn into_expr(&self, slot: SlotId) -> Expr {
match self {
EntityReference::EUID(euid) => Expr::val(euid.clone()),
EntityReference::Slot(loc) => Expr::slot(slot).with_maybe_source_loc(loc.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum UnexpectedSlotError {
#[error("found slot `{}` where slots are not allowed", .0.id)]
FoundSlot(Slot),
}
impl Diagnostic for UnexpectedSlotError {
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
match self {
Self::FoundSlot(Slot { loc, .. }) => loc.as_ref().map(|loc| {
let label = miette::LabeledSpan::underline(loc.span);
Box::new(std::iter::once(label)) as _
}),
}
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
match self {
Self::FoundSlot(Slot { loc, .. }) => loc.as_ref().map(|l| l as &dyn miette::SourceCode),
}
}
}
impl From<EntityUID> for EntityReference {
fn from(euid: EntityUID) -> Self {
Self::EUID(Arc::new(euid))
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum PrincipalOrResource {
Principal,
Resource,
}
impl std::fmt::Display for PrincipalOrResource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let v = Var::from(*self);
write!(f, "{v}")
}
}
impl TryFrom<Var> for PrincipalOrResource {
type Error = Var;
fn try_from(value: Var) -> Result<Self, Self::Error> {
match value {
Var::Principal => Ok(Self::Principal),
Var::Action => Err(Var::Action),
Var::Resource => Ok(Self::Resource),
Var::Context => Err(Var::Context),
}
}
}
#[derive(Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub enum PrincipalOrResourceConstraint {
Any,
In(EntityReference),
Eq(EntityReference),
Is(Arc<EntityType>),
IsIn(Arc<EntityType>, EntityReference),
}
impl PrincipalOrResourceConstraint {
pub fn any() -> Self {
PrincipalOrResourceConstraint::Any
}
pub fn is_eq(euid: Arc<EntityUID>) -> Self {
PrincipalOrResourceConstraint::Eq(EntityReference::euid(euid))
}
pub fn is_eq_slot() -> Self {
PrincipalOrResourceConstraint::Eq(EntityReference::Slot(None))
}
pub fn is_in_slot() -> Self {
PrincipalOrResourceConstraint::In(EntityReference::Slot(None))
}
pub fn is_in(euid: Arc<EntityUID>) -> Self {
PrincipalOrResourceConstraint::In(EntityReference::euid(euid))
}
pub fn is_entity_type_in_slot(entity_type: Arc<EntityType>) -> Self {
PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::Slot(None))
}
pub fn is_entity_type_in(entity_type: Arc<EntityType>, in_entity: Arc<EntityUID>) -> Self {
PrincipalOrResourceConstraint::IsIn(entity_type, EntityReference::euid(in_entity))
}
pub fn is_entity_type(entity_type: Arc<EntityType>) -> Self {
PrincipalOrResourceConstraint::Is(entity_type)
}
pub fn as_expr(&self, v: PrincipalOrResource) -> Expr {
match self {
PrincipalOrResourceConstraint::Any => Expr::val(true),
PrincipalOrResourceConstraint::Eq(euid) => {
Expr::is_eq(Expr::var(v.into()), euid.into_expr(v.into()))
}
PrincipalOrResourceConstraint::In(euid) => {
Expr::is_in(Expr::var(v.into()), euid.into_expr(v.into()))
}
PrincipalOrResourceConstraint::IsIn(entity_type, euid) => Expr::and(
Expr::is_entity_type(Expr::var(v.into()), entity_type.as_ref().clone()),
Expr::is_in(Expr::var(v.into()), euid.into_expr(v.into())),
),
PrincipalOrResourceConstraint::Is(entity_type) => {
Expr::is_entity_type(Expr::var(v.into()), entity_type.as_ref().clone())
}
}
}
pub fn display(&self, v: PrincipalOrResource) -> String {
match self {
PrincipalOrResourceConstraint::In(euid) => {
format!("{} in {}", v, euid.into_expr(v.into()))
}
PrincipalOrResourceConstraint::Eq(euid) => {
format!("{} == {}", v, euid.into_expr(v.into()))
}
PrincipalOrResourceConstraint::IsIn(entity_type, euid) => {
format!("{} is {} in {}", v, entity_type, euid.into_expr(v.into()))
}
PrincipalOrResourceConstraint::Is(entity_type) => {
format!("{v} is {entity_type}")
}
PrincipalOrResourceConstraint::Any => format!("{v}"),
}
}
pub fn get_euid(&self) -> Option<&Arc<EntityUID>> {
match self {
PrincipalOrResourceConstraint::Any => None,
PrincipalOrResourceConstraint::In(EntityReference::EUID(euid)) => Some(euid),
PrincipalOrResourceConstraint::In(EntityReference::Slot(_)) => None,
PrincipalOrResourceConstraint::Eq(EntityReference::EUID(euid)) => Some(euid),
PrincipalOrResourceConstraint::Eq(EntityReference::Slot(_)) => None,
PrincipalOrResourceConstraint::IsIn(_, EntityReference::EUID(euid)) => Some(euid),
PrincipalOrResourceConstraint::IsIn(_, EntityReference::Slot(_)) => None,
PrincipalOrResourceConstraint::Is(_) => None,
}
}
pub fn iter_entity_type_names(&self) -> impl Iterator<Item = &'_ EntityType> {
self.get_euid()
.into_iter()
.map(|euid| euid.entity_type())
.chain(match self {
PrincipalOrResourceConstraint::Is(entity_type)
| PrincipalOrResourceConstraint::IsIn(entity_type, _) => Some(entity_type.as_ref()),
_ => None,
})
}
}
#[derive(Clone, Hash, Eq, PartialEq, PartialOrd, Ord, Debug)]
pub enum ActionConstraint {
Any,
In(Vec<Arc<EntityUID>>),
Eq(Arc<EntityUID>),
#[cfg(feature = "tolerant-ast")]
ErrorConstraint,
}
impl std::fmt::Display for ActionConstraint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let render_euids =
|euids: &Vec<Arc<EntityUID>>| euids.iter().map(|euid| format!("{euid}")).join(",");
match self {
ActionConstraint::Any => write!(f, "action"),
ActionConstraint::In(euids) => {
write!(f, "action in [{}]", render_euids(euids))
}
ActionConstraint::Eq(euid) => write!(f, "action == {euid}"),
#[cfg(feature = "tolerant-ast")]
ActionConstraint::ErrorConstraint => write!(f, "<invalid_action_constraint>"),
}
}
}
impl ActionConstraint {
pub fn any() -> Self {
ActionConstraint::Any
}
pub fn is_in(euids: impl IntoIterator<Item = EntityUID>) -> Self {
ActionConstraint::In(euids.into_iter().map(Arc::new).collect())
}
pub fn is_eq(euid: EntityUID) -> Self {
ActionConstraint::Eq(Arc::new(euid))
}
fn euids_into_expr(euids: impl IntoIterator<Item = Arc<EntityUID>>) -> Expr {
Expr::set(euids.into_iter().map(Expr::val))
}
pub fn as_expr(&self) -> Expr {
match self {
ActionConstraint::Any => Expr::val(true),
ActionConstraint::In(euids) => Expr::is_in(
Expr::var(Var::Action),
ActionConstraint::euids_into_expr(euids.iter().cloned()),
),
ActionConstraint::Eq(euid) => {
Expr::is_eq(Expr::var(Var::Action), Expr::val(euid.clone()))
}
#[cfg(feature = "tolerant-ast")]
ActionConstraint::ErrorConstraint => Expr::new(
ExprKind::Error {
error_kind: AstExprErrorKind::InvalidExpr(
"Invalid action constraint".to_string(),
),
},
None,
(),
),
}
}
pub fn iter_euids(&self) -> impl Iterator<Item = &'_ EntityUID> {
match self {
ActionConstraint::Any => EntityIterator::None,
ActionConstraint::In(euids) => {
EntityIterator::Bunch(euids.iter().map(Arc::as_ref).collect())
}
ActionConstraint::Eq(euid) => EntityIterator::One(euid),
#[cfg(feature = "tolerant-ast")]
ActionConstraint::ErrorConstraint => EntityIterator::None,
}
}
pub fn iter_entity_type_names(&self) -> impl Iterator<Item = &'_ EntityType> {
self.iter_euids().map(|euid| euid.entity_type())
}
pub fn contains_only_action_types(self) -> Result<Self, NonEmpty<Arc<EntityUID>>> {
match self {
ActionConstraint::Any => Ok(self),
ActionConstraint::In(ref euids) => {
if let Some(euids) =
NonEmpty::collect(euids.iter().filter(|euid| !euid.is_action()).cloned())
{
Err(euids)
} else {
Ok(self)
}
}
ActionConstraint::Eq(ref euid) => {
if euid.is_action() {
Ok(self)
} else {
Err(nonempty![euid.clone()])
}
}
#[cfg(feature = "tolerant-ast")]
ActionConstraint::ErrorConstraint => Ok(self),
}
}
}
impl std::fmt::Display for StaticPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let policy_template = &self.0;
match policy_template {
TemplateBody::TemplateBody(template_body_impl) => {
for (k, v) in template_body_impl.annotations.iter() {
writeln!(f, "@{}(\"{}\")", k, v.val.escape_debug())?
}
write!(
f,
"{}(\n {},\n {},\n {}\n)",
self.effect(),
self.principal_constraint(),
self.action_constraint(),
self.resource_constraint(),
)?;
if let Some(non_scope_constraints) = self.non_scope_constraints() {
write!(f, " when {{\n {non_scope_constraints}\n}};")
} else {
write!(f, ";")
}
}
#[cfg(feature = "tolerant-ast")]
TemplateBody::TemplateBodyError(policy_id, _) => {
write!(f, "TemplateBody::TemplateBodyError({policy_id})")
}
}
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Hash)]
pub struct PolicyID(SmolStr);
impl PolicyID {
pub fn from_string(id: impl AsRef<str>) -> Self {
Self(SmolStr::from(id.as_ref()))
}
pub fn from_smolstr(id: SmolStr) -> Self {
Self(id)
}
pub fn into_smolstr(self) -> SmolStr {
self.0
}
}
impl std::fmt::Display for PolicyID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0.escape_debug())
}
}
impl AsRef<str> for PolicyID {
fn as_ref(&self) -> &str {
&self.0
}
}
#[cfg(feature = "arbitrary")]
impl arbitrary::Arbitrary<'_> for PolicyID {
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<PolicyID> {
let s: String = u.arbitrary()?;
Ok(PolicyID::from_string(s))
}
fn size_hint(depth: usize) -> (usize, Option<usize>) {
<String as arbitrary::Arbitrary>::size_hint(depth)
}
}
#[derive(Serialize, Deserialize, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[cfg_attr(feature = "wasm", tsify(into_wasm_abi, from_wasm_abi))]
pub enum Effect {
Permit,
Forbid,
}
impl std::fmt::Display for Effect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Permit => write!(f, "permit"),
Self::Forbid => write!(f, "forbid"),
}
}
}
enum EntityIterator<'a> {
None,
One(&'a EntityUID),
Bunch(Vec<&'a EntityUID>),
}
impl<'a> Iterator for EntityIterator<'a> {
type Item = &'a EntityUID;
fn next(&mut self) -> Option<Self::Item> {
match self {
EntityIterator::None => None,
EntityIterator::One(euid) => {
let eptr = *euid;
let mut ptr = EntityIterator::None;
std::mem::swap(self, &mut ptr);
Some(eptr)
}
EntityIterator::Bunch(v) => v.pop(),
}
}
}
#[cfg(test)]
pub(crate) mod test_generators {
use super::*;
pub fn all_por_constraints() -> impl Iterator<Item = PrincipalOrResourceConstraint> {
let euid = Arc::new(EntityUID::with_eid("test"));
let v = vec![
PrincipalOrResourceConstraint::any(),
PrincipalOrResourceConstraint::is_eq(euid.clone()),
PrincipalOrResourceConstraint::Eq(EntityReference::Slot(None)),
PrincipalOrResourceConstraint::is_in(euid),
PrincipalOrResourceConstraint::In(EntityReference::Slot(None)),
];
v.into_iter()
}
pub fn all_principal_constraints() -> impl Iterator<Item = PrincipalConstraint> {
all_por_constraints().map(|constraint| PrincipalConstraint { constraint })
}
pub fn all_resource_constraints() -> impl Iterator<Item = ResourceConstraint> {
all_por_constraints().map(|constraint| ResourceConstraint { constraint })
}
pub fn all_actions_constraints() -> impl Iterator<Item = ActionConstraint> {
let euid: EntityUID = "Action::\"test\""
.parse()
.expect("Invalid action constraint euid");
let v = vec![
ActionConstraint::any(),
ActionConstraint::is_eq(euid.clone()),
ActionConstraint::is_in([euid.clone()]),
ActionConstraint::is_in([euid.clone(), euid]),
];
v.into_iter()
}
pub fn all_templates() -> impl Iterator<Item = Template> {
let mut buf = vec![];
let permit = PolicyID::from_string("permit");
let forbid = PolicyID::from_string("forbid");
for principal in all_principal_constraints() {
for action in all_actions_constraints() {
for resource in all_resource_constraints() {
let permit = Template::new(
permit.clone(),
None,
Annotations::new(),
Effect::Permit,
principal.clone(),
action.clone(),
resource.clone(),
None,
);
let forbid = Template::new(
forbid.clone(),
None,
Annotations::new(),
Effect::Forbid,
principal.clone(),
action.clone(),
resource.clone(),
None,
);
buf.push(permit);
buf.push(forbid);
}
}
}
buf.into_iter()
}
}
#[cfg(test)]
mod test {
use cool_asserts::assert_matches;
use std::collections::HashSet;
use super::{test_generators::*, *};
use crate::{
parser::{
parse_policy,
test_utils::{expect_exactly_one_error, expect_some_error_matches},
},
test_utils::ExpectedErrorMessageBuilder,
};
#[test]
fn link_templates() {
for template in all_templates() {
template.check_invariant();
let t = Arc::new(template);
let env = t
.slots()
.map(|slot| (slot.id, EntityUID::with_eid("eid")))
.collect();
let _ = Template::link(t, PolicyID::from_string("id"), env).expect("Linking failed");
}
}
#[test]
fn test_template_rebuild() {
for template in all_templates() {
let id = template.id().clone();
let effect = template.effect();
let p = template.principal_constraint().clone();
let a = template.action_constraint().clone();
let r = template.resource_constraint().clone();
let non_scope = template.non_scope_constraints().cloned();
let t2 = Template::new(id, None, Annotations::new(), effect, p, a, r, non_scope);
assert_eq!(template, t2);
}
}
#[test]
fn test_static_policy_rebuild() {
for template in all_templates() {
if let Ok(ip) = StaticPolicy::try_from(template.clone()) {
let id = ip.id().clone();
let e = ip.effect();
let anno = ip
.annotations()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let p = ip.principal_constraint().clone();
let a = ip.action_constraint().clone();
let r = ip.resource_constraint().clone();
let non_scope = ip.non_scope_constraints();
let ip2 = StaticPolicy::new(id, None, anno, e, p, a, r, non_scope.cloned())
.expect("Policy Creation Failed");
assert_eq!(ip, ip2);
let (t2, inst) = Template::link_static_policy(ip2);
assert!(inst.is_static());
assert_eq!(&template, t2.as_ref());
}
}
}
#[test]
fn ir_binding_too_many() {
let tid = PolicyID::from_string("tid");
let iid = PolicyID::from_string("iid");
let t = Arc::new(Template::new(
tid,
None,
Annotations::new(),
Effect::Forbid,
PrincipalConstraint::is_eq_slot(),
ActionConstraint::Any,
ResourceConstraint::any(),
None,
));
let mut m = HashMap::new();
m.insert(SlotId::resource(), EntityUID::with_eid("eid"));
assert_matches!(Template::link(t, iid, m), Err(LinkingError::ArityError { unbound_values, extra_values }) => {
assert_eq!(unbound_values, vec![SlotId::principal()]);
assert_eq!(extra_values, vec![SlotId::resource()]);
});
}
#[test]
fn ir_binding_too_few() {
let tid = PolicyID::from_string("tid");
let iid = PolicyID::from_string("iid");
let t = Arc::new(Template::new(
tid,
None,
Annotations::new(),
Effect::Forbid,
PrincipalConstraint::is_eq_slot(),
ActionConstraint::Any,
ResourceConstraint::is_in_slot(),
None,
));
assert_matches!(Template::link(t.clone(), iid.clone(), HashMap::new()), Err(LinkingError::ArityError { unbound_values, extra_values }) => {
assert_eq!(unbound_values, vec![SlotId::resource(), SlotId::principal()]);
assert_eq!(extra_values, vec![]);
});
let mut m = HashMap::new();
m.insert(SlotId::principal(), EntityUID::with_eid("eid"));
assert_matches!(Template::link(t, iid, m), Err(LinkingError::ArityError { unbound_values, extra_values }) => {
assert_eq!(unbound_values, vec![SlotId::resource()]);
assert_eq!(extra_values, vec![]);
});
}
#[test]
fn ir_binding() {
let tid = PolicyID::from_string("template");
let iid = PolicyID::from_string("linked");
let t = Arc::new(Template::new(
tid,
None,
Annotations::new(),
Effect::Permit,
PrincipalConstraint::is_in_slot(),
ActionConstraint::any(),
ResourceConstraint::is_eq_slot(),
None,
));
let mut m = HashMap::new();
m.insert(SlotId::principal(), EntityUID::with_eid("theprincipal"));
m.insert(SlotId::resource(), EntityUID::with_eid("theresource"));
let r = Template::link(t, iid.clone(), m).expect("Should Succeed");
assert_eq!(r.id(), &iid);
assert_eq!(
r.env().get(&SlotId::principal()),
Some(&EntityUID::with_eid("theprincipal"))
);
assert_eq!(
r.env().get(&SlotId::resource()),
Some(&EntityUID::with_eid("theresource"))
);
}
#[test]
fn isnt_template_implies_from_succeeds() {
for template in all_templates() {
if template.slots().count() == 0 {
StaticPolicy::try_from(template).expect("Should succeed");
}
}
}
#[test]
fn is_template_implies_from_fails() {
for template in all_templates() {
if template.slots().count() != 0 {
assert!(
StaticPolicy::try_from(template.clone()).is_err(),
"Following template did convert {template}"
);
}
}
}
#[test]
fn non_template_iso() {
for template in all_templates() {
if let Ok(p) = StaticPolicy::try_from(template.clone()) {
let (t2, _) = Template::link_static_policy(p);
assert_eq!(&template, t2.as_ref());
}
}
}
#[test]
fn template_into_expr() {
for template in all_templates() {
if let Ok(p) = StaticPolicy::try_from(template.clone()) {
let t: Template = template;
assert_eq!(p.condition(), t.condition());
assert_eq!(p.effect(), t.effect());
}
}
}
#[test]
fn template_por_iter() {
let e = Arc::new(EntityUID::with_eid("eid"));
assert_eq!(PrincipalOrResourceConstraint::Any.get_euid(), None);
assert_eq!(
PrincipalOrResourceConstraint::In(EntityReference::EUID(e.clone())).get_euid(),
Some(&e)
);
assert_eq!(
PrincipalOrResourceConstraint::In(EntityReference::Slot(None)).get_euid(),
None
);
assert_eq!(
PrincipalOrResourceConstraint::Eq(EntityReference::EUID(e.clone())).get_euid(),
Some(&e)
);
assert_eq!(
PrincipalOrResourceConstraint::Eq(EntityReference::Slot(None)).get_euid(),
None
);
assert_eq!(
PrincipalOrResourceConstraint::IsIn(
Arc::new("T".parse().unwrap()),
EntityReference::EUID(e.clone())
)
.get_euid(),
Some(&e)
);
assert_eq!(
PrincipalOrResourceConstraint::Is(Arc::new("T".parse().unwrap())).get_euid(),
None
);
assert_eq!(
PrincipalOrResourceConstraint::IsIn(
Arc::new("T".parse().unwrap()),
EntityReference::Slot(None)
)
.get_euid(),
None
);
}
#[test]
fn action_iter() {
assert_eq!(ActionConstraint::Any.iter_euids().count(), 0);
let a = ActionConstraint::Eq(Arc::new(EntityUID::with_eid("test")));
let v = a.iter_euids().collect::<Vec<_>>();
assert_eq!(vec![&EntityUID::with_eid("test")], v);
let a =
ActionConstraint::is_in([EntityUID::with_eid("test1"), EntityUID::with_eid("test2")]);
let set = a.iter_euids().collect::<HashSet<_>>();
let e1 = EntityUID::with_eid("test1");
let e2 = EntityUID::with_eid("test2");
let correct = vec![&e1, &e2].into_iter().collect::<HashSet<_>>();
assert_eq!(set, correct);
}
#[test]
fn test_iter_none() {
let mut i = EntityIterator::None;
assert_eq!(i.next(), None);
}
#[test]
fn test_iter_once() {
let id = EntityUID::from_components(
name::Name::parse_unqualified_name("s").unwrap().into(),
entity::Eid::new("eid"),
None,
);
let mut i = EntityIterator::One(&id);
assert_eq!(i.next(), Some(&id));
assert_eq!(i.next(), None);
}
#[test]
fn test_iter_mult() {
let id1 = EntityUID::from_components(
name::Name::parse_unqualified_name("s").unwrap().into(),
entity::Eid::new("eid1"),
None,
);
let id2 = EntityUID::from_components(
name::Name::parse_unqualified_name("s").unwrap().into(),
entity::Eid::new("eid2"),
None,
);
let v = vec![&id1, &id2];
let mut i = EntityIterator::Bunch(v);
assert_eq!(i.next(), Some(&id2));
assert_eq!(i.next(), Some(&id1));
assert_eq!(i.next(), None)
}
#[test]
fn euid_into_expr() {
let e = EntityReference::Slot(None);
assert_eq!(
e.into_expr(SlotId::principal()),
Expr::slot(SlotId::principal())
);
let e = EntityReference::euid(Arc::new(EntityUID::with_eid("eid")));
assert_eq!(
e.into_expr(SlotId::principal()),
Expr::val(EntityUID::with_eid("eid"))
);
}
#[test]
fn por_constraint_display() {
let t = PrincipalOrResourceConstraint::Eq(EntityReference::Slot(None));
let s = t.display(PrincipalOrResource::Principal);
assert_eq!(s, "principal == ?principal");
let t = PrincipalOrResourceConstraint::Eq(EntityReference::euid(Arc::new(
EntityUID::with_eid("test"),
)));
let s = t.display(PrincipalOrResource::Principal);
assert_eq!(s, "principal == test_entity_type::\"test\"");
}
#[test]
fn unexpected_templates() {
let policy_str = r#"permit(principal == ?principal, action, resource);"#;
assert_matches!(parse_policy(Some(PolicyID::from_string("id")), policy_str), Err(e) => {
expect_exactly_one_error(policy_str, &e, &ExpectedErrorMessageBuilder::error(
"expected a static policy, got a template containing the slot ?principal"
)
.help("try removing the template slot(s) from this policy")
.exactly_one_underline("?principal")
.build()
);
});
let policy_str =
r#"permit(principal == ?principal, action, resource) when { ?principal == 3 } ;"#;
assert_matches!(parse_policy(Some(PolicyID::from_string("id")), policy_str), Err(e) => {
expect_some_error_matches(policy_str, &e, &ExpectedErrorMessageBuilder::error(
"expected a static policy, got a template containing the slot ?principal"
)
.help("try removing the template slot(s) from this policy")
.exactly_one_underline("?principal")
.build()
);
assert_eq!(e.len(), 2);
});
}
#[test]
fn policy_to_expr() {
let policy_str = r#"permit(principal is A, action, resource is B)
when { 1 == 2 }
unless { 2 == 1}
when { 3 == 4}
unless { 4 == 3};"#;
assert_matches!(parse_policy(Some(PolicyID::from_string("id")), policy_str), Ok(p) => {
assert_eq!(ToString::to_string(&p.condition()), "(principal is A) && (true && ((resource is B) && ((1 == 2) && ((!(2 == 1)) && ((3 == 4) && (!(4 == 3)))))))");
});
}
#[cfg(feature = "tolerant-ast")]
#[test]
fn template_body_error_methods() {
use std::str::FromStr;
let policy_id = PolicyID::from_string("error_policy");
let error_loc = Some(Loc::new(0..1, "ASTErrorNode".into()));
let error_body = TemplateBody::TemplateBodyError(policy_id.clone(), error_loc.clone());
let expected_error = <ExprWithErrsBuilder as ExprBuilder>::new()
.error(ParseErrors::singleton(ToASTError::new(
ToASTErrorKind::ASTErrorNode,
Some(Loc::new(0..1, "ASTErrorNode".into())),
)))
.unwrap();
assert_eq!(error_body.id(), &policy_id);
assert_eq!(error_body.loc(), error_loc.as_ref());
let new_policy_id = PolicyID::from_string("new_error_policy");
let updated_error_body = error_body.new_id(new_policy_id.clone());
assert_matches!(updated_error_body,
TemplateBody::TemplateBodyError(id, loc) if id == new_policy_id && loc == error_loc
);
assert_eq!(error_body.effect(), Effect::Forbid);
assert_eq!(
error_body.annotation(&AnyId::from_str("test").unwrap()),
None
);
assert!(error_body.annotations().count() == 0);
assert_eq!(
*error_body.principal_constraint(),
PrincipalConstraint::any()
);
assert_eq!(*error_body.action_constraint(), ActionConstraint::any());
assert_eq!(*error_body.resource_constraint(), ResourceConstraint::any());
assert_eq!(error_body.non_scope_constraints(), Some(&expected_error));
assert_eq!(error_body.condition(), expected_error);
let display_str = format!("{error_body}");
assert!(display_str.contains("TemplateBodyError"));
assert!(display_str.contains("error_policy"));
}
#[cfg(feature = "tolerant-ast")]
#[test]
fn template_error_methods() {
let policy_id = PolicyID::from_string("error_policy");
let error_loc = Some(Loc::new(0..1, "ASTErrorNode".into()));
let error_template = Template::error(policy_id.clone(), error_loc.clone());
assert_eq!(error_template.id(), &policy_id);
assert!(error_template.slots().count() == 0);
assert_matches!(error_template.body,
TemplateBody::TemplateBodyError(ref id, ref loc) if id == &policy_id && loc == &error_loc
);
assert_eq!(
error_template.principal_constraint(),
&PrincipalConstraint::any()
);
assert_eq!(*error_template.action_constraint(), ActionConstraint::any());
assert_eq!(
*error_template.resource_constraint(),
ResourceConstraint::any()
);
assert_eq!(error_template.effect(), Effect::Forbid);
assert_eq!(
error_template.condition(),
DEFAULT_ERROR_EXPR.as_ref().clone()
);
assert_eq!(error_template.loc(), error_loc.as_ref());
assert!(error_template.annotations().count() == 0);
let display_str = format!("{error_template}");
assert!(display_str.contains("TemplateBody::TemplateBodyError"));
assert!(display_str.contains(&policy_id.to_string()));
}
}