use std::collections::{BTreeMap, HashMap, HashSet};
use std::sync::Arc;
use cedar_policy_core::ast::{self, Value};
use cedar_policy_core::authorizer::Decision;
use cedar_policy_core::batched_evaluator::is_authorized_batched;
use cedar_policy_core::batched_evaluator::{
err::BatchedEvalError, EntityLoader as EntityLoaderInternal,
};
use cedar_policy_core::evaluator::{EvaluationError, RestrictedEvaluator};
use cedar_policy_core::extensions::Extensions;
use cedar_policy_core::tpe;
use itertools::Itertools;
use ref_cast::RefCast;
use smol_str::SmolStr;
use crate::{
api, tpe_err, Authorizer, Context, Entities, Entity, EntityId, EntityTypeName, EntityUid,
PartialEntityError, PartialRequestCreationError, PermissionQueryError, Policy, PolicyId,
PolicySet, Request, RequestValidationError, RestrictedExpression, Schema,
TpeReauthorizationError,
};
#[doc = include_str!("../../experimental_warning.md")]
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
pub struct PartialEntityUid(pub(crate) tpe::request::PartialEntityUID);
#[doc(hidden)]
impl AsRef<tpe::request::PartialEntityUID> for PartialEntityUid {
fn as_ref(&self) -> &tpe::request::PartialEntityUID {
&self.0
}
}
impl PartialEntityUid {
pub fn new(ty: EntityTypeName, id: Option<EntityId>) -> Self {
Self(tpe::request::PartialEntityUID {
ty: ty.0,
eid: id.map(|id| <EntityId as AsRef<ast::Eid>>::as_ref(&id).clone()),
})
}
pub fn from_concrete(euid: EntityUid) -> Self {
let (ty, eid) = euid.0.components();
Self(tpe::request::PartialEntityUID { ty, eid: Some(eid) })
}
}
#[doc = include_str!("../../experimental_warning.md")]
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
pub struct PartialRequest(pub(crate) tpe::request::PartialRequest);
#[doc(hidden)]
impl AsRef<tpe::request::PartialRequest> for PartialRequest {
fn as_ref(&self) -> &tpe::request::PartialRequest {
&self.0
}
}
impl PartialRequest {
pub fn new(
principal: PartialEntityUid,
action: EntityUid,
resource: PartialEntityUid,
context: Option<Context>,
schema: &Schema,
) -> Result<Self, PartialRequestCreationError> {
let context = context
.map(|c| match c.0 {
ast::Context::RestrictedResidual(_) => {
Err(PartialRequestCreationError::ContextContainsUnknowns)
}
ast::Context::Value(m) => Ok(m),
})
.transpose()?;
tpe::request::PartialRequest::new(principal.0, action.0, resource.0, context, &schema.0)
.map(Self)
.map_err(|e| PartialRequestCreationError::Validation(e.into()))
}
}
#[doc = include_str!("../../experimental_warning.md")]
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
pub struct ResourceQueryRequest(pub(crate) PartialRequest);
impl ResourceQueryRequest {
pub fn new(
principal: EntityUid,
action: EntityUid,
resource: EntityTypeName,
context: Context,
schema: &Schema,
) -> Result<Self, PartialRequestCreationError> {
PartialRequest::new(
PartialEntityUid(principal.0.into()),
action,
PartialEntityUid::new(resource, None),
Some(context),
schema,
)
.map(Self)
}
fn principal(&self) -> EntityUid {
#[expect(
clippy::unwrap_used,
reason = "constructor requires concrete principal"
)]
EntityUid(self.0 .0.principal().clone().try_into().unwrap())
}
fn context(&self) -> Context {
#[expect(clippy::unwrap_used, reason = "constructor requires concrete context")]
let context_attrs = self.0 .0.context_attrs().unwrap();
#[expect(
clippy::unwrap_used,
reason = "building context from BTreeMap iter, so no duplicates are possible"
)]
Context::from_pairs(
context_attrs
.iter()
.map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
)
.unwrap()
}
pub fn to_request(
&self,
resource_id: EntityId,
schema: Option<&Schema>,
) -> Result<Request, RequestValidationError> {
Request::new(
self.principal(),
EntityUid(self.0 .0.action().clone()),
EntityUid::from_type_name_and_id(
EntityTypeName(self.0 .0.resource_type().clone()),
resource_id,
),
self.context(),
schema,
)
}
}
#[doc = include_str!("../../experimental_warning.md")]
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
pub struct PrincipalQueryRequest(pub(crate) PartialRequest);
impl PrincipalQueryRequest {
pub fn new(
principal: EntityTypeName,
action: EntityUid,
resource: EntityUid,
context: Context,
schema: &Schema,
) -> Result<Self, PartialRequestCreationError> {
PartialRequest::new(
PartialEntityUid::new(principal, None),
action,
PartialEntityUid(resource.0.into()),
Some(context),
schema,
)
.map(Self)
}
fn resource(&self) -> EntityUid {
#[expect(clippy::unwrap_used, reason = "constructor requires concrete resource")]
EntityUid(self.0 .0.resource().clone().try_into().unwrap())
}
fn context(&self) -> Context {
#[expect(clippy::unwrap_used, reason = "constructor requires concrete context")]
let context_attrs = self.0 .0.context_attrs().unwrap();
#[expect(
clippy::unwrap_used,
reason = "building context from BTreeMap iter, so no duplicates are possible"
)]
Context::from_pairs(
context_attrs
.iter()
.map(|(a, v)| (a.to_string(), RestrictedExpression(v.clone().into()))),
)
.unwrap()
}
pub fn to_request(
&self,
principal_id: EntityId,
schema: Option<&Schema>,
) -> Result<Request, RequestValidationError> {
Request::new(
EntityUid::from_type_name_and_id(
EntityTypeName(self.0 .0.principal_type().clone()),
principal_id,
),
EntityUid(self.0 .0.action().clone()),
self.resource(),
self.context(),
schema,
)
}
}
#[doc = include_str!("../../experimental_warning.md")]
#[derive(Debug, Clone)]
pub struct ActionQueryRequest {
principal: PartialEntityUid,
resource: PartialEntityUid,
context: Option<Arc<BTreeMap<SmolStr, Value>>>,
schema: Schema,
}
impl ActionQueryRequest {
pub fn new(
principal: PartialEntityUid,
resource: PartialEntityUid,
context: Option<Context>,
schema: Schema,
) -> Result<Self, PartialRequestCreationError> {
let context = context
.map(|c| match c.0 {
ast::Context::RestrictedResidual(_) => {
Err(PartialRequestCreationError::ContextContainsUnknowns)
}
ast::Context::Value(m) => Ok(m),
})
.transpose()?;
Ok(Self {
principal,
resource,
context,
schema,
})
}
fn partial_request(
&self,
action: EntityUid,
) -> Result<PartialRequest, cedar_policy_core::validator::RequestValidationError> {
tpe::request::PartialRequest::new(
self.principal.0.clone(),
action.0,
self.resource.0.clone(),
self.context.clone(),
&self.schema.0,
)
.map(PartialRequest)
}
}
#[doc = include_str!("../../experimental_warning.md")]
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
pub struct PartialEntity(pub(crate) tpe::entities::PartialEntity);
impl PartialEntity {
pub fn new(
uid: EntityUid,
attrs: Option<BTreeMap<SmolStr, RestrictedExpression>>,
ancestors: Option<HashSet<EntityUid>>,
tags: Option<BTreeMap<SmolStr, RestrictedExpression>>,
schema: &Schema,
) -> Result<Self, PartialEntityError> {
Ok(Self(tpe::entities::PartialEntity::new(
uid.0,
attrs
.map(|ps| {
ps.into_iter()
.map(|(k, v)| {
Ok((
k,
RestrictedEvaluator::new(Extensions::all_available())
.interpret(v.0.as_borrowed())?,
))
})
.collect::<Result<BTreeMap<_, _>, EvaluationError>>()
})
.transpose()?,
ancestors.map(|s| s.into_iter().map(|e| e.0).collect()),
tags.map(|ps| {
ps.into_iter()
.map(|(k, v)| {
Ok((
k,
RestrictedEvaluator::new(Extensions::all_available())
.interpret(v.0.as_borrowed())?,
))
})
.collect::<Result<BTreeMap<_, _>, EvaluationError>>()
})
.transpose()?,
&schema.0,
)?))
}
}
#[doc = include_str!("../../experimental_warning.md")]
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
pub struct PartialEntities(pub(crate) tpe::entities::PartialEntities);
#[doc(hidden)]
impl AsRef<tpe::entities::PartialEntities> for PartialEntities {
fn as_ref(&self) -> &tpe::entities::PartialEntities {
&self.0
}
}
impl PartialEntities {
pub fn from_json_value(
value: serde_json::Value,
schema: &Schema,
) -> Result<Self, tpe_err::EntitiesError> {
tpe::entities::PartialEntities::from_json_value(value, &schema.0).map(Self)
}
pub fn from_concrete(
entities: Entities,
schema: &Schema,
) -> Result<Self, tpe_err::EntitiesError> {
tpe::entities::PartialEntities::from_concrete(entities.0, &schema.0).map(Self)
}
pub fn empty() -> Self {
Self(tpe::entities::PartialEntities::new())
}
pub fn from_partial_entities(
entities: impl IntoIterator<Item = PartialEntity>,
schema: &Schema,
) -> Result<Self, tpe_err::EntitiesError> {
Ok(Self(tpe::entities::PartialEntities::from_entities(
entities.into_iter().map(|entity| entity.0),
&schema.0,
)?))
}
}
#[doc = include_str!("../../experimental_warning.md")]
#[repr(transparent)]
#[derive(Debug, Clone, RefCast)]
pub struct TpeResponse<'a>(pub(crate) tpe::response::Response<'a>);
#[doc(hidden)]
impl<'a> AsRef<tpe::response::Response<'a>> for TpeResponse<'a> {
fn as_ref(&self) -> &tpe::response::Response<'a> {
&self.0
}
}
impl TpeResponse<'_> {
pub fn decision(&self) -> Option<Decision> {
self.0.decision()
}
pub fn reason(&self) -> Option<impl Iterator<Item = &PolicyId>> {
Some(self.0.reason()?.map(PolicyId::ref_cast))
}
pub fn residual_permits(&self) -> impl Iterator<Item = &PolicyId> {
self.0
.residual_permits()
.map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
}
pub fn true_permits(&self) -> impl Iterator<Item = &PolicyId> {
self.0
.true_permits()
.map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
}
pub fn false_permits(&self) -> impl Iterator<Item = &PolicyId> {
self.0
.false_permits()
.map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
}
pub fn error_permits(&self) -> impl Iterator<Item = &PolicyId> {
self.0
.error_permits()
.map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
}
pub fn residual_forbids(&self) -> impl Iterator<Item = &PolicyId> {
self.0
.residual_forbids()
.map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
}
pub fn true_forbids(&self) -> impl Iterator<Item = &PolicyId> {
self.0
.true_forbids()
.map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
}
pub fn false_forbids(&self) -> impl Iterator<Item = &PolicyId> {
self.0
.false_forbids()
.map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
}
pub fn error_forbids(&self) -> impl Iterator<Item = &PolicyId> {
self.0
.error_forbids()
.map(|rp| PolicyId::ref_cast(rp.get_policy_id()))
}
pub fn reauthorize(
&self,
request: &Request,
entities: &Entities,
) -> Result<api::Response, TpeReauthorizationError> {
self.0
.reauthorize(&request.0, &entities.0)
.map(Into::into)
.map_err(Into::into)
}
pub fn residual_policies(&self) -> impl Iterator<Item = Policy> + '_ {
self.0
.residual_permits()
.chain(self.0.residual_forbids())
.map(|p| Policy::from_ast(p.clone().into()))
}
pub fn policies(&self) -> impl Iterator<Item = Policy> + '_ {
self.0
.policies()
.map(|p| Policy::from_ast(p.clone().into()))
}
pub fn policy_set(&self) -> PolicySet {
PolicySet::from_ast(self.0.policy_set())
}
#[deprecated(
since = "4.12.0",
note = "TpeResponse::residual_policies now returns only non-trivial residual policies"
)]
pub fn nontrivial_residual_policies(&'_ self) -> impl Iterator<Item = Policy> + '_ {
self.residual_policies()
}
pub fn get_policy(&self, id: &PolicyId) -> Option<Policy> {
self.0
.get_residual_policy(id.as_ref())
.map(|p| Policy::from_ast(p.clone().into()))
}
}
#[doc = include_str!("../../experimental_warning.md")]
pub trait EntityLoader {
fn load_entities(&mut self, uids: &HashSet<EntityUid>) -> HashMap<EntityUid, Option<Entity>>;
}
struct EntityLoaderWrapper<'a>(&'a mut dyn EntityLoader);
impl EntityLoaderInternal for EntityLoaderWrapper<'_> {
fn load_entities(
&mut self,
uids: &HashSet<ast::EntityUID>,
) -> HashMap<ast::EntityUID, Option<ast::Entity>> {
let ids = uids
.iter()
.map(|id| EntityUid::ref_cast(id).clone())
.collect();
self.0
.load_entities(&ids)
.into_iter()
.map(|(uid, entity)| (uid.0, entity.map(|e| e.0)))
.collect()
}
}
#[doc = include_str!("../../experimental_warning.md")]
#[derive(Debug)]
pub struct TestEntityLoader<'a> {
entities: &'a Entities,
}
impl<'a> TestEntityLoader<'a> {
pub fn new(entities: &'a Entities) -> Self {
Self { entities }
}
}
impl EntityLoader for TestEntityLoader<'_> {
fn load_entities(&mut self, uids: &HashSet<EntityUid>) -> HashMap<EntityUid, Option<Entity>> {
uids.iter()
.map(|uid| {
let entity = self.entities.get(uid).cloned();
(uid.clone(), entity)
})
.collect()
}
}
impl PolicySet {
#[doc = include_str!("../../experimental_warning.md")]
pub fn tpe<'a>(
&self,
request: &'a PartialRequest,
entities: &'a PartialEntities,
schema: &'a Schema,
) -> Result<TpeResponse<'a>, tpe_err::TpeError> {
use cedar_policy_core::tpe::is_authorized;
let ps = &self.ast;
let res = is_authorized(ps, &request.0, &entities.0, &schema.0)?;
Ok(TpeResponse(res))
}
#[doc = include_str!("../../experimental_warning.md")]
pub fn is_authorized_batched(
&self,
query: &Request,
schema: &Schema,
loader: &mut dyn EntityLoader,
max_iters: u32,
) -> Result<Decision, BatchedEvalError> {
is_authorized_batched(
&query.0,
&self.ast,
&schema.0,
&mut EntityLoaderWrapper(loader),
max_iters,
)
}
#[doc = include_str!("../../experimental_warning.md")]
pub fn query_resource(
&self,
request: &ResourceQueryRequest,
entities: &Entities,
schema: &Schema,
) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
let tpe_response = self.tpe(&request.0, &partial_entities, schema)?;
let policies = tpe_response.policy_set();
match tpe_response.decision() {
Some(Decision::Allow) => Ok(entities
.iter()
.filter(|entity| entity.0.uid().entity_type() == request.0.0.resource_type())
.map(Entity::uid)
.collect_vec()
.into_iter()),
Some(Decision::Deny) => Ok(vec![].into_iter()),
None => Ok(entities
.iter()
.filter(|entity| entity.0.uid().entity_type() == request.0.0.resource_type())
.filter(|entity| {
#[expect(
clippy::unwrap_used, reason = "`to_request` cannot panic because we do not pass a schema. However, the correctness of the authorization
decision depends on having valid a request and entities, but we do not do any validation here. Entities were already validated by
`PartialEntities::from_concrete`. The request was _mostly_ validated by its constructor, but the concrete request could still be invalid
if the resource entity is an enum entity and the id is not an instance of that enum. This cannot happen here because we draw candidate
resources from the entities, which we know are valid."
)]
let req = request.to_request(entity.uid().id().clone(), None).unwrap();
let authorizer = Authorizer::new();
let auth_response = authorizer
.is_authorized(
&req,
&policies,
entities,
);
auth_response.decision() == Decision::Allow
})
.map(Entity::uid)
.collect_vec()
.into_iter()),
}
}
#[doc = include_str!("../../experimental_warning.md")]
pub fn query_principal(
&self,
request: &PrincipalQueryRequest,
entities: &Entities,
schema: &Schema,
) -> Result<impl Iterator<Item = EntityUid>, PermissionQueryError> {
let partial_entities = PartialEntities::from_concrete(entities.clone(), schema)?;
let tpe_response = self.tpe(&request.0, &partial_entities, schema)?;
let policies = tpe_response.policy_set();
match tpe_response.decision() {
Some(Decision::Allow) => Ok(entities
.iter()
.filter(|entity| entity.0.uid().entity_type() == request.0.0.principal_type())
.map(Entity::uid)
.collect_vec()
.into_iter()),
Some(Decision::Deny) => Ok(vec![].into_iter()),
None => Ok(entities
.iter()
.filter(|entity| entity.0.uid().entity_type() == request.0.0.principal_type())
.filter(|entity| {
#[expect(
clippy::unwrap_used, reason = "`to_request` cannot panic because we do not pass a schema. However, the correctness of the authorization
decision depends on having valid a request and entities, but we do not do any validation here. Entities were already validated by
`PartialEntities::from_concrete`. The request was _mostly_ validated by its constructor, but the concrete request could still be invalid
if the principal entity is an enum entity and the id is not an instance of that enum. This cannot happen here because we draw candidate
principals from the entities, which we know are valid."
)]
let req = request.to_request(entity.uid().id().clone(), None).unwrap();
let authorizer = Authorizer::new();
let auth_response = authorizer
.is_authorized(
&req,
&policies,
entities,
);
auth_response.decision() == Decision::Allow
})
.map(Entity::uid)
.collect_vec()
.into_iter()),
}
}
#[doc = include_str!("../../experimental_warning.md")]
pub fn query_action<'a>(
&self,
request: &'a ActionQueryRequest,
entities: &PartialEntities,
) -> Result<impl Iterator<Item = (&'a EntityUid, Option<Decision>)>, PermissionQueryError> {
let mut authorized_actions = Vec::new();
for action in request
.schema
.0
.actions_for_principal_and_resource(&request.principal.0.ty, &request.resource.0.ty)
{
if let Ok(partial_request) = request.partial_request(action.clone().into()) {
let decision = self
.tpe(&partial_request, entities, &request.schema)?
.decision();
if decision != Some(Decision::Deny) {
authorized_actions.push((RefCast::ref_cast(action), decision));
}
}
}
Ok(authorized_actions.into_iter())
}
}
#[cfg(test)]
mod tpe_tests {
use std::{
collections::{BTreeMap, HashSet},
str::FromStr,
};
use cedar_policy_core::tpe::err::EntitiesError;
use cool_asserts::assert_matches;
use crate::{PartialEntity, PartialEntityError, RestrictedExpression, Schema};
#[test]
fn entity_construction() {
let schema = Schema::from_str(
r"
entity A in B tags Long;
entity B;
",
)
.unwrap();
PartialEntity::new(
r#"A::"foo""#.parse().unwrap(),
None,
Some(HashSet::from_iter([r#"B::"b""#.parse().unwrap()])),
Some(BTreeMap::from_iter([(
"".into(),
RestrictedExpression::new_long(1),
)])),
&schema,
)
.unwrap();
assert_matches!(
PartialEntity::new(
r#"A::"foo""#.parse().unwrap(),
None,
Some(HashSet::from_iter([r#"C::"c""#.parse().unwrap()])),
Some(BTreeMap::from_iter([(
"".into(),
RestrictedExpression::new_long(1)
)])),
&schema
),
Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
);
assert_matches!(
PartialEntity::new(
r#"A::"foo""#.parse().unwrap(),
None,
Some(HashSet::from_iter([r#"B::"b""#.parse().unwrap()])),
Some(BTreeMap::from_iter([(
"".into(),
RestrictedExpression::new_bool(true)
)])),
&schema
),
Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
);
}
mod streaming_service {
use std::{collections::BTreeMap, str::FromStr};
use cedar_policy_core::{authorizer::Decision, tpe::err::EntitiesError};
use cool_asserts::assert_matches;
use itertools::Itertools;
use similar_asserts::assert_eq;
use crate::{
ActionConstraint, ActionQueryRequest, Context, Entities, EntityId, EntityUid,
PartialEntities, PartialEntity, PartialEntityError, PartialEntityUid, PartialRequest,
PolicySet, PrincipalConstraint, PrincipalQueryRequest, Request, ResourceConstraint,
ResourceQueryRequest, RestrictedExpression, Schema,
};
#[test]
fn entities_construction() {
let schema = schema();
PartialEntity::new(
r#"Movie::"foo""#.parse().unwrap(),
None,
None,
None,
&schema,
)
.unwrap();
PartialEntity::new(
r#"Show::"foo""#.parse().unwrap(),
Some(BTreeMap::from_iter([
("isFree".into(), RestrictedExpression::new_bool(true)),
(
"releaseDate".into(),
RestrictedExpression::new_datetime("2025-01-01"),
),
(
"isEarlyAccess".into(),
RestrictedExpression::new_bool(false),
),
])),
None,
None,
&schema,
)
.unwrap();
assert_matches!(
PartialEntity::new(
r#"Show::"foo""#.parse().unwrap(),
Some(BTreeMap::from_iter([
("isFree".into(), RestrictedExpression::new_bool(true)),
(
"isEarlyAccess".into(),
RestrictedExpression::new_bool(false)
),
])),
None,
None,
&schema
),
Err(PartialEntityError::Entities(EntitiesError::Validation(_)))
);
let e1 = PartialEntity::new(
r#"Show::"foo""#.parse().unwrap(),
Some(BTreeMap::from_iter([
("isFree".into(), RestrictedExpression::new_bool(true)),
(
"releaseDate".into(),
RestrictedExpression::new_datetime("2025-01-01"),
),
(
"isEarlyAccess".into(),
RestrictedExpression::new_bool(false),
),
])),
None,
None,
&schema,
)
.unwrap();
let e2 = PartialEntity::new(
r#"Subscriber::"a""#.parse().unwrap(),
None,
None,
None,
&schema,
)
.unwrap();
PartialEntities::from_partial_entities([e1.clone(), e2.clone()], &schema).unwrap();
let e3 = PartialEntity::new(
r#"Show::"foo""#.parse().unwrap(),
Some(BTreeMap::from_iter([
("isFree".into(), RestrictedExpression::new_bool(true)),
(
"releaseDate".into(),
RestrictedExpression::new_datetime("2025-01-01"),
),
("isEarlyAccess".into(), RestrictedExpression::new_bool(true)),
])),
None,
None,
&schema,
)
.unwrap();
assert_matches!(
PartialEntities::from_partial_entities([e1, e2, e3], &schema),
Err(EntitiesError::Duplicate(_)),
);
}
#[track_caller]
fn schema() -> Schema {
Schema::from_cedarschema_str(
r"
// Types
type Subscription = {
tier: String
};
type Profile = {
isKid: Bool
};
// Entities
entity FreeMember;
entity Subscriber = {
subscription: Subscription,
profile: Profile
};
entity Movie = {
isFree: Bool,
needsRentOrBuy: Bool,
isOscarNominated: Bool
};
entity Show = {
isFree: Bool,
releaseDate: datetime,
isEarlyAccess: Bool
};
// Actions for content in general
action watch
appliesTo {
principal: [FreeMember, Subscriber],
resource: [Movie, Show],
context: {
now: {
datetime: datetime,
localTimeOffset: duration
}
}
};
// Actions for movies only
action rent, buy
appliesTo {
principal: [FreeMember, Subscriber],
resource: Movie,
context: {
now: {
datetime: datetime
}
}
};
",
)
.unwrap()
.0
}
#[track_caller]
fn policy_set() -> PolicySet {
PolicySet::from_str(
r#"
// Subscriber Content Access (Shows)
@id("subscriber-content-access/show")
permit (
principal is Subscriber,
action == Action::"watch",
resource is Show
)
unless
{ resource.isEarlyAccess && context.now.datetime < resource.releaseDate };
// Subscriber Content Access (Movies)
@id("subscriber-content-access/movie")
permit (
principal is Subscriber,
action == Action::"watch",
resource is Movie
)
unless { resource.needsRentOrBuy };
// Free Content Access
@id("free-content-access")
permit (
principal is FreeMember,
action == Action::"watch",
resource
)
when { resource.isFree };
// Promo: Rent/Buy Oscar-Nominated Movies Until the Oscars
@id("rent-buy-oscar-movie")
permit (
principal is Subscriber,
action in [Action::"rent", Action::"buy"],
resource is Movie
)
when
{
resource.isOscarNominated &&
context.now.datetime >= datetime("2025-02-02T19:00:00-0500") &&
context.now.datetime < datetime(
"2025-03-02T19:00:00-0500"
) // Oscars Night
};
// Early Access (24h) to Shows for Premium Subscribers
@id("early-access-show")
permit (
principal is Subscriber,
action == Action::"watch",
resource is Show
)
when
{
resource.isEarlyAccess &&
principal.subscription.tier == "premium" &&
context.now.datetime >= resource.releaseDate.offset(duration("-24h"))
};
// Forbid Bedtime Access to Kid Profile
@id("forbid-bedtime-watch-kid-profile")
forbid (
principal is Subscriber,
action == Action::"watch",
resource
)
when { principal.profile.isKid }
unless
{
// `toTime()` returns the duration modulo one day (i.e., it ignores the "date"
// component). Here, we use it to calculate the subscriber's local time and
// compare the result against durations that represent 6:00AM and 9:00PM.
duration("6h") <= context.now
.datetime
.offset
(
context.now.localTimeOffset
)
.toTime
(
) &&
context.now.datetime.offset(context.now.localTimeOffset).toTime() <= duration(
"21h"
)
};
"#,
)
.unwrap()
}
#[track_caller]
fn entities() -> Entities {
Entities::from_json_value(
serde_json::json!(
[
{
"uid": {
"type": "Subscriber",
"id": "Alice"
},
"attrs": {
"subscription" : {
"tier": "standard"
},
"profile" : {
"isKid": false
}
},
"parents": []
},
{
"uid": {
"type": "FreeMember",
"id": "Bob"
},
"attrs": {},
"parents": []
},
{
"uid": {
"type": "Subscriber",
"id": "Charlie"
},
"attrs": {
"subscription" : {
"tier": "premium"
},
"profile" : {
"isKid": false
}
},
"parents": []
},
{
"uid": {
"type": "Subscriber",
"id": "Dave"
},
"attrs": {
"subscription" : {
"tier": "standard"
},
"profile" : {
"isKid": true
}
},
"parents": []
},
{
"uid": {
"type": "Movie",
"id": "The Godparent"
},
"attrs": {
"isFree" : true,
"needsRentOrBuy" : false,
"isOscarNominated": true
},
"parents": []
},
{
"uid": {
"type": "Movie",
"id": "The Gleaming"
},
"attrs": {
"isFree" : false,
"needsRentOrBuy" : false,
"isOscarNominated": false
},
"parents": []
},
{
"uid": {
"type": "Movie",
"id": "Devilish"
},
"attrs": {
"isFree" : false,
"needsRentOrBuy" : true,
"isOscarNominated": true
},
"parents": []
},
{
"uid": {
"type": "Show",
"id": "Buddies"
},
"attrs": {
"isFree" : false,
"releaseDate": "2024-10-10",
"isEarlyAccess": false
},
"parents": []
},
{
"uid": {
"type": "Show",
"id": "Breach"
},
"attrs": {
"isFree" : false,
"releaseDate": "2025-02-21",
"isEarlyAccess": true
},
"parents": []
}
]
),
Some(&schema()),
)
.unwrap()
}
#[test]
fn run_tpe() {
let schema = schema();
let request = PartialRequest::new(
PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
r#"Action::"watch""#.parse().unwrap(),
PartialEntityUid::new("Movie".parse().unwrap(), None),
Some(
Context::from_pairs([(
"now".into(),
RestrictedExpression::new_record([
(
"datetime".into(),
RestrictedExpression::from_str(r#"datetime("2025-07-22")"#)
.unwrap(),
),
(
"localTimeOffset".into(),
RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
),
])
.unwrap(),
)])
.unwrap(),
),
&schema,
)
.unwrap();
let policies = policy_set();
let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
let response = policies
.tpe(&request, &partial_entities, &schema)
.expect("tpe should succeed");
assert_eq!(response.policies().count(), policies.num_of_policies());
for p in response.residual_policies() {
assert_matches!(p.action_constraint(), ActionConstraint::Any);
assert_matches!(p.principal_constraint(), PrincipalConstraint::Any);
assert_matches!(p.resource_constraint(), ResourceConstraint::Any);
}
assert_eq!(
response
.residual_policies()
.next()
.unwrap()
.annotation("id")
.unwrap(),
"subscriber-content-access/movie"
);
assert_eq!(response.decision(), None);
assert!(response.reason().is_none());
let request = Request::new(
EntityUid::from_type_name_and_id(
"Subscriber".parse().unwrap(),
EntityId::new("Alice"),
),
r#"Action::"watch""#.parse().unwrap(),
EntityUid::from_type_name_and_id(
"Movie".parse().unwrap(),
EntityId::new("The Godparent"),
),
Context::from_pairs([(
"now".into(),
RestrictedExpression::new_record([
(
"datetime".into(),
RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
),
(
"localTimeOffset".into(),
RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
),
])
.unwrap(),
)])
.unwrap(),
Some(&schema),
)
.unwrap();
assert_matches!(response.reauthorize(&request, &entities()), Ok(res) => {
assert_eq!(res.decision(), Decision::Allow);
});
let request = Request::new(
EntityUid::from_type_name_and_id(
"Subscriber".parse().unwrap(),
EntityId::new("Alice"),
),
r#"Action::"watch""#.parse().unwrap(),
EntityUid::from_type_name_and_id(
"Movie".parse().unwrap(),
EntityId::new("Devilish"),
),
Context::from_pairs([(
"now".into(),
RestrictedExpression::new_record([
(
"datetime".into(),
RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
),
(
"localTimeOffset".into(),
RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
),
])
.unwrap(),
)])
.unwrap(),
Some(&schema),
)
.unwrap();
assert_matches!(response.reauthorize(&request, &entities()), Ok(res) => {
assert_eq!(res.decision(), Decision::Deny);
});
}
#[test]
fn query_resource() {
let schema = schema();
let policies = policy_set();
let request = ResourceQueryRequest::new(
r#"Subscriber::"Alice""#.parse().unwrap(),
r#"Action::"watch""#.parse().unwrap(),
"Movie".parse().unwrap(),
Context::from_pairs([(
"now".into(),
RestrictedExpression::new_record([
(
"datetime".into(),
RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
),
(
"localTimeOffset".into(),
RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
),
])
.unwrap(),
)])
.unwrap(),
&schema,
)
.unwrap();
let movies = policies
.query_resource(&request, &entities(), &schema)
.unwrap()
.sorted()
.collect_vec();
assert_eq!(
movies,
&[
EntityUid::from_str(r#"Movie::"The Gleaming""#).unwrap(),
EntityUid::from_str(r#"Movie::"The Godparent""#).unwrap(),
]
);
}
#[test]
fn query_principal() {
let schema = schema();
let policies = policy_set();
let request = PrincipalQueryRequest::new(
"Subscriber".parse().unwrap(),
r#"Action::"watch""#.parse().unwrap(),
r#"Movie::"The Godparent""#.parse().unwrap(),
Context::from_pairs([(
"now".into(),
RestrictedExpression::new_record([
(
"datetime".into(),
RestrictedExpression::from_str(r#"datetime("2025-07-22")"#).unwrap(),
),
(
"localTimeOffset".into(),
RestrictedExpression::from_str(r#"duration("0h")"#).unwrap(),
),
])
.unwrap(),
)])
.unwrap(),
&schema,
)
.unwrap();
let subscribers = policies
.query_principal(&request, &entities(), &schema)
.unwrap()
.sorted()
.collect_vec();
assert_eq!(
subscribers,
&[
EntityUid::from_str(r#"Subscriber::"Alice""#).unwrap(),
EntityUid::from_str(r#"Subscriber::"Charlie""#).unwrap(),
]
);
}
#[test]
fn query_action_alice() {
let schema = schema();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"Subscriber::"Alice""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Movie::"The Godparent""#.parse().unwrap()),
None,
schema.clone(),
)
.unwrap();
let policies = policy_set();
let mut actions: Vec<_> = policies
.query_action(
&request,
&PartialEntities::from_concrete(entities(), &schema).unwrap(),
)
.unwrap()
.collect();
actions.sort_by_key(|(a, _)| *a);
assert_eq!(
actions,
vec![
(&r#"Action::"buy""#.parse().unwrap(), None),
(&r#"Action::"rent""#.parse().unwrap(), None),
(
&r#"Action::"watch""#.parse().unwrap(),
Some(Decision::Allow)
),
]
);
}
#[test]
fn query_action_bob_free() {
let schema = schema();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"FreeMember::"Bob""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Movie::"The Godparent""#.parse().unwrap()),
None,
schema.clone(),
)
.unwrap();
let policies = policy_set();
let actions: Vec<_> = policies
.query_action(
&request,
&PartialEntities::from_concrete(entities(), &schema).unwrap(),
)
.unwrap()
.collect();
assert_eq!(
actions,
vec![(
&r#"Action::"watch""#.parse().unwrap(),
Some(Decision::Allow)
),]
);
}
#[test]
fn query_action_bob_not_free() {
let schema = schema();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"FreeMember::"Bob""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Movie::"The Gleaming""#.parse().unwrap()),
None,
schema.clone(),
)
.unwrap();
let policies = policy_set();
let actions: Vec<_> = policies
.query_action(
&request,
&PartialEntities::from_concrete(entities(), &schema).unwrap(),
)
.unwrap()
.collect();
assert_eq!(actions, vec![]);
}
}
mod github {
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
use cedar_policy_core::tpe::err::TpeError;
use cedar_policy_core::{authorizer::Decision, batched_evaluator::err::BatchedEvalError};
use cool_asserts::assert_matches;
use itertools::Itertools;
use similar_asserts::assert_eq;
use crate::{
ActionQueryRequest, Context, Entities, EntityUid, PartialEntities, PartialEntityUid,
PolicySet, PrincipalQueryRequest, Request, ResourceQueryRequest, RestrictedExpression,
Schema, TestEntityLoader,
};
#[track_caller]
fn schema() -> Schema {
Schema::from_str(
r#"
entity Team, UserGroup in [UserGroup];
entity Issue = {
"repo": Repository,
"reporter": User,
};
entity Org = {
"members": UserGroup,
"owners": UserGroup,
};
entity Repository = {
"admins": UserGroup,
"maintainers": UserGroup,
"readers": UserGroup,
"triagers": UserGroup,
"writers": UserGroup,
};
entity User in [UserGroup, Team];
action push, pull, fork appliesTo {
principal: [User],
resource: [Repository]
};
action assign_issue, delete_issue, edit_issue appliesTo {
principal: [User],
resource: [Issue]
};
action add_reader, add_writer, add_maintainer, add_admin, add_triager appliesTo {
principal: [User],
resource: [Repository]
};
"#,
)
.unwrap()
}
fn policy_set() -> PolicySet {
PolicySet::from_str(
r#"
//Actions for readers
permit (
principal,
action == Action::"pull",
resource
)
when { principal in resource.readers };
permit (
principal,
action == Action::"fork",
resource
)
when { principal in resource.readers };
permit (
principal,
action == Action::"delete_issue",
resource
)
when { principal in resource.repo.readers && principal == resource.reporter };
permit (
principal,
action == Action::"edit_issue",
resource
)
when { principal in resource.repo.readers && principal == resource.reporter };
//Actions for triagers
permit (
principal,
action == Action::"assign_issue",
resource
)
when { principal in resource.repo.triagers };
//Actions for writers
permit (
principal,
action == Action::"push",
resource
)
when { principal in resource.writers };
permit (
principal,
action == Action::"edit_issue",
resource
)
when { principal in resource.repo.writers };
//Actions for maintainers
permit (
principal,
action == Action::"delete_issue",
resource
)
when { principal in resource.repo.maintainers };
//Actions for admins
permit (
principal,
action in
[Action::"add_reader",
Action::"add_triager",
Action::"add_writer",
Action::"add_maintainer",
Action::"add_admin"],
resource
)
when { principal in resource.admins };
//We use the same permissions for org owners, and rely on placing them in the admins group for every repository in the org
//The other option is to duplicate all policies for the org base permissions (with a separate heirarchy for each org)
"#,
)
.unwrap()
}
#[track_caller]
fn entities() -> Entities {
Entities::from_json_value(serde_json::json!(
[
{
"uid": { "__entity": { "type": "User", "id": "alice"} },
"attrs": {},
"parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} }, { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} } ]
},
{
"uid": { "__entity": { "type": "User", "id": "jane"} },
"attrs": {},
"parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} }, { "__entity": { "type": "Team", "id": "team_that_can_read_everything"} }]
},
{
"uid": { "__entity": { "type": "User", "id": "bob"} },
"attrs": {},
"parents": []
},
{
"uid": { "__entity": { "type": "Repository", "id": "common_knowledge"} },
"attrs": {
"readers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} },
"triagers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"} },
"writers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} },
"maintainers" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },
"admins" : { "__entity": { "type": "UserGroup", "id": "common_knowledge_admins"} }
},
"parents": []
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} },
"attrs": {
},
"parents": [ ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"} },
"attrs": {
},
"parents": [ { "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} } ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_writers"} },
"attrs": {
},
"parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_triagers"}} ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"} },
"attrs": {
},
"parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_writers"}} ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "common_knowledge_admins"} },
"attrs": {
},
"parents": [ {"__entity": { "type": "UserGroup", "id": "common_knowledge_maintainers"}} ]
},
{
"uid": { "__entity": { "type": "Repository", "id": "secret"} },
"attrs": {
"readers" : { "__entity": { "type": "UserGroup", "id": "secret_readers"} },
"triagers" : { "__entity": { "type": "UserGroup", "id": "secret_triagers"} },
"writers" : { "__entity": { "type": "UserGroup", "id": "secret_writers"} },
"maintainers" : { "__entity": { "type": "UserGroup", "id": "secret_maintainers"} },
"admins" : { "__entity": { "type": "UserGroup", "id": "secret_admins"} }
},
"parents": []
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "secret_readers"} },
"attrs": {
},
"parents": [ ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "secret_triagers"} },
"attrs": {
},
"parents": [ { "__entity": { "type": "UserGroup", "id": "secret_readers"} } ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "secret_writers"} },
"attrs": {
},
"parents": [ {"__entity": { "type": "UserGroup", "id": "secret_triagers"}} ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "secret_maintainers"} },
"attrs": {
},
"parents": [ {"__entity": { "type": "UserGroup", "id": "secret_writers"}} ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "secret_admins"} },
"attrs": {
},
"parents": [ {"__entity": { "type": "UserGroup", "id": "secret_maintainers"}} ]
},
{
"uid": { "__entity": { "type": "Repository", "id": "uncommon_knowledge"} },
"attrs": {
"readers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} },
"triagers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"} },
"writers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} },
"maintainers" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"} },
"admins" : { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_admins"} }
},
"parents": []
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} },
"attrs": {
},
"parents": [ ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"} },
"attrs": {
},
"parents": [ { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} } ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"} },
"attrs": {
},
"parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_triagers"}} ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"} },
"attrs": {
},
"parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_writers"}} ]
},
{
"uid": { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_admins"} },
"attrs": {
},
"parents": [ {"__entity": { "type": "UserGroup", "id": "uncommon_knowledge_maintainers"}} ]
},
{
"uid": { "__entity": { "type": "Team", "id": "team_that_can_read_everything"} },
"attrs": {},
"parents": [{ "__entity": { "type": "UserGroup", "id": "common_knowledge_readers"} }, { "__entity": { "type": "UserGroup", "id": "secret_readers"} }, { "__entity": { "type": "UserGroup", "id": "uncommon_knowledge_readers"} }]
},
]
), Some(&schema())).unwrap()
}
#[test]
fn query_resource() {
let schema = schema();
let request = ResourceQueryRequest::new(
r#"User::"jane""#.parse().unwrap(),
r#"Action::"push""#.parse().unwrap(),
"Repository".parse().unwrap(),
Context::empty(),
&schema,
)
.unwrap();
let policies = policy_set();
assert_matches!(&policies.query_resource(&request, &entities(), &schema).unwrap().collect_vec(), [uid] => {
assert_eq!(uid, &r#"Repository::"common_knowledge""#.parse().unwrap());
});
}
#[test]
fn query_principal() {
let schema = schema();
let request = PrincipalQueryRequest::new(
r"User".parse().unwrap(),
r#"Action::"pull""#.parse().unwrap(),
r#"Repository::"secret""#.parse().unwrap(),
Context::empty(),
&schema,
)
.unwrap();
let policies = policy_set();
assert_matches!(&policies.query_principal(&request, &entities(), &schema).unwrap().collect_vec(), [uid] => {
assert_eq!(uid, &r#"User::"jane""#.parse().unwrap());
});
}
#[test]
fn query_action() {
let schema = schema();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"User::"jane""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Repository::"secret""#.parse().unwrap()),
None,
schema.clone(),
)
.unwrap();
let policies = policy_set();
let mut actions: Vec<_> = policies
.query_action(
&request,
&PartialEntities::from_concrete(entities(), &schema).unwrap(),
)
.unwrap()
.collect();
actions.sort_by_key(|(a, _)| *a);
assert_eq!(
actions,
vec![
(&r#"Action::"fork""#.parse().unwrap(), Some(Decision::Allow)),
(&r#"Action::"pull""#.parse().unwrap(), Some(Decision::Allow)),
]
);
}
#[test]
fn test_is_authorized_vs_is_authorized_batched() {
use crate::{Authorizer, Request};
let schema = schema();
let policies = policy_set();
let entities = entities();
let authorizer = Authorizer::new();
let test_requests = vec![
Request::new(
r#"User::"alice""#.parse().unwrap(),
r#"Action::"push""#.parse().unwrap(),
r#"Repository::"common_knowledge""#.parse().unwrap(),
Context::empty(),
Some(&schema),
)
.unwrap(),
Request::new(
r#"User::"jane""#.parse().unwrap(),
r#"Action::"pull""#.parse().unwrap(),
r#"Repository::"secret""#.parse().unwrap(),
Context::empty(),
Some(&schema),
)
.unwrap(),
Request::new(
r#"User::"bob""#.parse().unwrap(),
r#"Action::"push""#.parse().unwrap(),
r#"Repository::"common_knowledge""#.parse().unwrap(),
Context::empty(),
Some(&schema),
)
.unwrap(),
Request::new(
r#"User::"alice""#.parse().unwrap(),
r#"Action::"fork""#.parse().unwrap(),
r#"Repository::"common_knowledge""#.parse().unwrap(),
Context::empty(),
Some(&schema),
)
.unwrap(),
];
for (i, request) in test_requests.iter().enumerate() {
let standard_response = authorizer.is_authorized(request, &policies, &entities);
let mut loader = TestEntityLoader::new(&entities);
let batched_decision = policies
.is_authorized_batched(request, &schema, &mut loader, u32::MAX)
.unwrap();
let standard_decision = standard_response.decision();
assert_eq!(
standard_decision,
batched_decision,
"Request {}: is_authorized returned {:?} but is_authorized_batched returned {:?}",
i + 1,
standard_decision,
batched_decision
);
}
}
#[test]
fn test_batched_evaluation_error_validation() {
let schema = schema();
let policies = PolicySet::from_str(
r#"permit(principal, action, resource) when { principal.nonexistent_attr == "value" };"#
).unwrap();
let request = Request::new(
EntityUid::from_str("User::\"alice\"").unwrap(),
EntityUid::from_str("Action::\"push\"").unwrap(),
EntityUid::from_str("Repository::\"repo\"").unwrap(),
Context::empty(),
Some(&schema),
)
.unwrap();
let entities = entities();
let mut loader = TestEntityLoader::new(&entities);
let result = policies.is_authorized_batched(&request, &schema, &mut loader, 10);
assert!(matches!(
result,
Err(BatchedEvalError::TPE(TpeError::Validation(_)))
));
}
#[test]
#[cfg(feature = "partial-eval")]
fn test_batched_evaluation_error_partial_request() {
let context_with_unknown = Context::from_pairs([(
"key".to_string(),
RestrictedExpression::new_unknown("test_unknown"),
)])
.unwrap();
let request = Request::new(
EntityUid::from_str("User::\"alice\"").unwrap(),
EntityUid::from_str("Action::\"view\"").unwrap(),
EntityUid::from_str("Resource::\"doc\"").unwrap(),
context_with_unknown,
None,
)
.unwrap();
let schema = schema();
let pset = PolicySet::from_str("permit(principal, action, resource);").unwrap();
let entities = Entities::empty();
let mut loader = TestEntityLoader::new(&entities);
let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
assert_matches!(result, Err(BatchedEvalError::PartialRequest(_)));
}
#[test]
fn test_batched_evaluation_error_invalid_entity() {
struct InvalidEntityLoader;
impl crate::EntityLoader for InvalidEntityLoader {
fn load_entities(
&mut self,
_uids: &HashSet<EntityUid>,
) -> HashMap<EntityUid, Option<crate::Entity>> {
let mut result = HashMap::new();
let uid = EntityUid::from_strs("Org", "myorg");
let entity = crate::Entity::new(
uid.clone(),
[
(
"members".to_string(),
RestrictedExpression::new_string("not_a_usergroup".to_string()),
),
(
"owners".to_string(),
RestrictedExpression::new_entity_uid(EntityUid::from_strs(
"UserGroup",
"2",
)),
),
]
.into(),
HashSet::new(),
)
.unwrap();
result.insert(uid, Some(entity));
result
}
}
let schema = schema();
let pset = PolicySet::from_str(
"permit(principal, action, resource) when { Org::\"myorg\".members == UserGroup::\"1\"};",
)
.unwrap();
let request = Request::new(
r#"User::"alice""#.parse().unwrap(),
r#"Action::"push""#.parse().unwrap(),
r#"Repository::"common_knowledge""#.parse().unwrap(),
Context::empty(),
Some(&schema),
)
.unwrap();
let mut loader = InvalidEntityLoader;
let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
assert_matches!(result, Err(BatchedEvalError::Entities(_)));
}
#[test]
#[cfg(feature = "partial-eval")]
fn test_batched_evaluation_error_partial_entity() {
struct PartialEntityLoader;
impl crate::EntityLoader for PartialEntityLoader {
fn load_entities(
&mut self,
_uids: &HashSet<EntityUid>,
) -> HashMap<EntityUid, Option<crate::Entity>> {
let mut result = HashMap::new();
let uid = EntityUid::from_strs("Org", "myorg");
let entity = crate::Entity::new(
uid.clone(),
[
(
"members".to_string(),
RestrictedExpression::new_unknown("partial_members"),
),
(
"owners".to_string(),
RestrictedExpression::new_entity_uid(EntityUid::from_strs(
"UserGroup",
"2",
)),
),
]
.into(),
HashSet::new(),
)
.unwrap();
result.insert(uid, Some(entity));
result
}
}
let schema = schema();
let pset = PolicySet::from_str(
"permit(principal, action, resource) when { Org::\"myorg\".members == UserGroup::\"1\"};",
)
.unwrap();
let request = Request::new(
r#"User::"alice""#.parse().unwrap(),
r#"Action::"push""#.parse().unwrap(),
r#"Repository::"common_knowledge""#.parse().unwrap(),
Context::empty(),
Some(&schema),
)
.unwrap();
let mut loader = PartialEntityLoader;
let result = pset.is_authorized_batched(&request, &schema, &mut loader, 10);
assert_matches!(result, Err(BatchedEvalError::PartialValueToValue(_)));
}
#[test]
fn test_batched_evaluation_error_insufficient_iters() {
let schema = schema();
let policies = policy_set();
let entities = entities();
let request = Request::new(
r#"User::"alice""#.parse().unwrap(),
r#"Action::"push""#.parse().unwrap(),
r#"Repository::"common_knowledge""#.parse().unwrap(),
Context::empty(),
Some(&schema),
)
.unwrap();
let mut loader = TestEntityLoader::new(&entities);
let result = policies.is_authorized_batched(&request, &schema, &mut loader, 0);
assert_matches!(result, Err(BatchedEvalError::InsufficientIterations(_)));
}
}
mod trivial {
use cedar_policy_core::authorizer::Decision;
use itertools::Itertools;
use crate::{
Context, Entities, PartialEntities, PartialEntityUid, PartialRequest, PolicyId,
PolicySet, PrincipalQueryRequest, ResourceQueryRequest, Schema,
};
use std::{i64, str::FromStr};
fn schema() -> Schema {
Schema::from_str("entity P, R; action A appliesTo { principal: P, resource: R };")
.unwrap()
}
fn entities() -> Entities {
Entities::from_json_value(
serde_json::json!([
{ "uid": { "__entity": { "type": "P", "id": ""} }, "attrs": {}, "parents": [] },
{ "uid": { "__entity": { "type": "R", "id": ""} }, "attrs": {}, "parents": [] },
]),
None,
)
.unwrap()
}
#[test]
fn trivial_permit_tpe() {
let schema = schema();
let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
let req = PartialRequest::new(
PartialEntityUid::new("P".parse().unwrap(), None),
r#"Action::"A""#.parse().unwrap(),
PartialEntityUid::new("R".parse().unwrap(), None),
None,
&schema,
)
.unwrap();
let response = PolicySet::from_str(r"permit(principal, action, resource);")
.unwrap()
.tpe(&req, &partial_entities, &schema)
.unwrap();
assert_eq!(response.decision(), Some(Decision::Allow));
assert_eq!(
response.reason().unwrap().collect::<Vec<_>>(),
vec![&PolicyId::new("policy0")]
);
}
#[test]
fn trivial_permit_query_principal() {
let schema = schema();
let entities = entities();
let req = PrincipalQueryRequest::new(
"P".parse().unwrap(),
r#"Action::"A""#.parse().unwrap(),
r#"R::"""#.parse().unwrap(),
Context::empty(),
&schema,
)
.unwrap();
let principals = PolicySet::from_str(r#"permit(principal, action, resource);"#)
.unwrap()
.query_principal(&req, &entities, &schema)
.unwrap()
.collect_vec();
assert_eq!(&principals, &[r#"P::"""#.parse().unwrap()]);
}
#[test]
fn trivial_permit_query_resource() {
let schema = schema();
let entities = entities();
let req = ResourceQueryRequest::new(
r#"P::"""#.parse().unwrap(),
r#"Action::"A""#.parse().unwrap(),
"R".parse().unwrap(),
Context::empty(),
&schema,
)
.unwrap();
let resources = PolicySet::from_str(r#"permit(principal, action, resource);"#)
.unwrap()
.query_resource(&req, &entities, &schema)
.unwrap()
.collect_vec();
assert_eq!(&resources, &[r#"R::"""#.parse().unwrap()]);
}
#[test]
fn trivial_forbid_tpe() {
let schema = schema();
let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
let req = PartialRequest::new(
PartialEntityUid::new("P".parse().unwrap(), None),
r#"Action::"A""#.parse().unwrap(),
PartialEntityUid::new("R".parse().unwrap(), None),
None,
&schema,
)
.unwrap();
let response = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
.unwrap()
.tpe(&req, &partial_entities, &schema)
.unwrap();
assert_eq!(response.decision(), Some(Decision::Deny));
assert_eq!(
response.reason().unwrap().collect::<Vec<_>>(),
vec![&PolicyId::new("policy0")]
);
assert_eq!(
response.true_forbids().collect::<Vec<_>>(),
vec![&PolicyId::new("policy0")]
);
}
#[test]
fn trivial_forbid_query_principal() {
let schema = schema();
let entities = entities();
let req = PrincipalQueryRequest::new(
"P".parse().unwrap(),
r#"Action::"A""#.parse().unwrap(),
r#"R::"""#.parse().unwrap(),
Context::empty(),
&schema,
)
.unwrap();
let principals = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
.unwrap()
.query_principal(&req, &entities, &schema)
.unwrap()
.collect_vec();
assert_eq!(&principals, &[]);
}
#[test]
fn trivial_forbid_query_resource() {
let schema = schema();
let entities = entities();
let req = ResourceQueryRequest::new(
r#"P::"""#.parse().unwrap(),
r#"Action::"A""#.parse().unwrap(),
"R".parse().unwrap(),
Context::empty(),
&schema,
)
.unwrap();
let resources = PolicySet::from_str(r#"forbid(principal, action, resource);"#)
.unwrap()
.query_resource(&req, &entities, &schema)
.unwrap()
.collect_vec();
assert_eq!(&resources, &[]);
}
#[test]
fn error_tpe() {
let schema = schema();
let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
let req = PartialRequest::new(
PartialEntityUid::new("P".parse().unwrap(), None),
r#"Action::"A""#.parse().unwrap(),
PartialEntityUid::new("R".parse().unwrap(), None),
None,
&schema,
)
.unwrap();
let response = PolicySet::from_str(&format!(
r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
i64::MAX
))
.unwrap()
.tpe(&req, &partial_entities, &schema)
.unwrap();
assert_eq!(response.decision(), Some(Decision::Deny));
assert_eq!(
response.reason().unwrap().collect::<Vec<_>>(),
Vec::<&PolicyId>::new()
);
}
#[test]
fn error_query_principal() {
let schema = schema();
let entities = entities();
let req = PrincipalQueryRequest::new(
"P".parse().unwrap(),
r#"Action::"A""#.parse().unwrap(),
r#"R::"""#.parse().unwrap(),
Context::empty(),
&schema,
)
.unwrap();
let principals = PolicySet::from_str(&format!(
r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
i64::MAX
))
.unwrap()
.query_principal(&req, &entities, &schema)
.unwrap()
.collect_vec();
assert_eq!(&principals, &[]);
}
#[test]
fn error_query_resource() {
let schema = schema();
let entities = entities();
let req = ResourceQueryRequest::new(
r#"P::"""#.parse().unwrap(),
r#"Action::"A""#.parse().unwrap(),
"R".parse().unwrap(),
Context::empty(),
&schema,
)
.unwrap();
let resources = PolicySet::from_str(&format!(
r#"permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};"#,
i64::MAX
))
.unwrap()
.query_resource(&req, &entities, &schema)
.unwrap()
.collect_vec();
assert_eq!(&resources, &[]);
}
#[test]
fn empty_tpe() {
let schema = schema();
let partial_entities = PartialEntities::from_concrete(entities(), &schema).unwrap();
let req = PartialRequest::new(
PartialEntityUid::new("P".parse().unwrap(), None),
r#"Action::"A""#.parse().unwrap(),
PartialEntityUid::new("R".parse().unwrap(), None),
None,
&schema,
)
.unwrap();
let response = PolicySet::from_str(r#""#)
.unwrap()
.tpe(&req, &partial_entities, &schema)
.unwrap();
assert_eq!(response.decision(), Some(Decision::Deny));
assert_eq!(
response.reason().unwrap().collect::<Vec<_>>(),
Vec::<&PolicyId>::new()
);
}
#[test]
fn empty_query_principal() {
let schema = schema();
let entities = entities();
let req = PrincipalQueryRequest::new(
"P".parse().unwrap(),
r#"Action::"A""#.parse().unwrap(),
r#"R::"""#.parse().unwrap(),
Context::empty(),
&schema,
)
.unwrap();
let principals = PolicySet::from_str(r#""#)
.unwrap()
.query_principal(&req, &entities, &schema)
.unwrap()
.collect_vec();
assert_eq!(&principals, &[]);
}
#[test]
fn empty_query_resource() {
let schema = schema();
let entities = entities();
let req = ResourceQueryRequest::new(
r#"P::"""#.parse().unwrap(),
r#"Action::"A""#.parse().unwrap(),
"R".parse().unwrap(),
Context::empty(),
&schema,
)
.unwrap();
let resources = PolicySet::from_str(r#""#)
.unwrap()
.query_resource(&req, &entities, &schema)
.unwrap()
.collect_vec();
assert_eq!(&resources, &[]);
}
}
mod response_iterators {
use std::{i64, str::FromStr};
use cedar_policy_core::authorizer::Decision;
use crate::{
PartialEntities, PartialEntityUid, PartialRequest, PolicyId, PolicySet, Schema,
};
#[test]
fn all_policy_categories() {
let schema = Schema::from_str(
"entity P, R; action A appliesTo { principal: P, resource: R, context: { flag: Bool } };",
)
.unwrap();
let req = PartialRequest::new(
PartialEntityUid::new("P".parse().unwrap(), None),
r#"Action::"A""#.parse().unwrap(),
PartialEntityUid::new("R".parse().unwrap(), None),
None,
&schema,
)
.unwrap();
let policies = PolicySet::from_str(&format!(
r#"
permit(principal, action, resource);
permit(principal, action, resource) when {{ false }};
permit(principal, action, resource) when {{ ({} + 1) == 0 || true }};
permit(principal, action, resource) when {{ context.flag }};
forbid(principal, action, resource);
forbid(principal, action, resource) when {{ false }};
forbid(principal, action, resource) when {{ ({} + 1) == 0 || true }};
forbid(principal, action, resource) when {{ context.flag }};
"#,
i64::MAX,
i64::MAX
))
.unwrap();
let entities = PartialEntities::empty();
let response = policies.tpe(&req, &entities, &schema).unwrap();
assert_eq!(response.decision(), Some(Decision::Deny));
assert_eq!(
response.reason().unwrap().collect::<Vec<_>>(),
vec![&PolicyId::new("policy4")]
);
assert_eq!(
response.true_permits().collect::<Vec<_>>(),
vec![&PolicyId::new("policy0")]
);
assert_eq!(
response.false_permits().collect::<Vec<_>>(),
vec![&PolicyId::new("policy1")]
);
assert_eq!(
response.error_permits().collect::<Vec<_>>(),
vec![&PolicyId::new("policy2")]
);
assert_eq!(
response.residual_permits().collect::<Vec<_>>(),
vec![&PolicyId::new("policy3")]
);
assert_eq!(
response.true_forbids().collect::<Vec<_>>(),
vec![&PolicyId::new("policy4")]
);
assert_eq!(
response.false_forbids().collect::<Vec<_>>(),
vec![&PolicyId::new("policy5")]
);
assert_eq!(
response.error_forbids().collect::<Vec<_>>(),
vec![&PolicyId::new("policy6")]
);
assert_eq!(
response.residual_forbids().collect::<Vec<_>>(),
vec![&PolicyId::new("policy7")]
);
}
}
mod query_action {
use cedar_policy_core::authorizer::Decision;
use crate::{
ActionQueryRequest, Context, PartialEntities, PartialEntityUid, PolicySet, Schema,
};
use similar_asserts::assert_eq;
use std::str::FromStr;
#[test]
fn test() {
let policies = PolicySet::from_str(
r#"
// Edit might be alowed, depending on context
permit(principal, action == Action::"edit", resource)
when {
context.ip.isInRange(resource.allowed_edit_range)
};
// We pass a concrete resource, so we know this will be allowed
permit(principal, action == Action::"view", resource)
when {
resource.public
};
// never allowed for any request
forbid(principal, action == Action::"delete", resource);
// allowed for this action, but it doesn't apply to the request types
permit(principal, action == Action::"not_on_photo", resource);
"#,
)
.unwrap();
let schema = Schema::from_str(
"
entity User, Other;
entity Photo {
public: Bool,
allowed_edit_range: ipaddr,
};
action view, edit, delete appliesTo {
principal: User,
resource: Photo,
context: {
ip: ipaddr,
}
};
action not_on_photo appliesTo {
principal: User,
resource: Other
};
",
)
.unwrap();
let entities = PartialEntities::from_json_value(
serde_json::json!([
{
"uid": { "__entity": { "type": "Photo", "id": "vacation.jpg"} },
"attrs": {
"public": true,
"allowed_edit_range": "192.0.2.0/24"
},
"parents": []
},
]),
&schema,
)
.unwrap();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
None,
schema,
)
.unwrap();
let mut actions: Vec<_> = policies
.query_action(&request, &entities)
.unwrap()
.collect();
actions.sort_by_key(|(a, _)| *a);
assert_eq!(
actions,
vec![
(&r#"Action::"edit""#.parse().unwrap(), None),
(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow)),
]
)
}
#[test]
fn permitted_action() {
let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
let schema = Schema::from_str(
"entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
)
.unwrap();
let entities = PartialEntities::empty();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
None,
schema,
)
.unwrap();
let actions: Vec<_> = policies
.query_action(&request, &entities)
.unwrap()
.collect();
assert_eq!(
actions,
vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
);
}
#[test]
fn maybe_permitted_action() {
let policies = PolicySet::from_str(
"permit(principal, action, resource) when { context.should_allow };",
)
.unwrap();
let schema = Schema::from_str(
"entity User, Photo; action view appliesTo { principal: User, resource: Photo, context: {should_allow: Bool}};",
)
.unwrap();
let entities = PartialEntities::empty();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
None,
schema,
)
.unwrap();
let actions: Vec<_> = policies
.query_action(&request, &entities)
.unwrap()
.collect();
assert_eq!(actions, vec![(&r#"Action::"view""#.parse().unwrap(), None)]);
}
#[test]
fn forbidden_action() {
let policies = PolicySet::from_str("forbid(principal, action, resource);").unwrap();
let schema = Schema::from_str(
"entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
)
.unwrap();
let entities = PartialEntities::empty();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
None,
schema,
)
.unwrap();
let actions: Vec<_> = policies
.query_action(&request, &entities)
.unwrap()
.collect();
assert_eq!(actions, Vec::new(),);
}
#[test]
fn invalid_permitted_action() {
let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
let schema = Schema::from_str("entity User, Photo, Other; action view appliesTo { principal: User, resource: Other};").unwrap();
let entities = PartialEntities::empty();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
None,
schema,
)
.unwrap();
let actions: Vec<_> = policies
.query_action(&request, &entities)
.unwrap()
.collect();
assert_eq!(actions, Vec::new());
}
#[test]
fn invalid_context_permitted_action() {
let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
let schema = Schema::from_str("entity User, Photo; action view appliesTo { principal: User, resource: Photo, context: {a: Long}};").unwrap();
let entities = PartialEntities::empty();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
Some(Context::empty()),
schema,
)
.unwrap();
let actions: Vec<_> = policies
.query_action(&request, &entities)
.unwrap()
.collect();
assert_eq!(actions, Vec::new());
}
#[test]
fn no_actions_in_schema() {
let policies = PolicySet::from_str("permit(principal, action, resource);").unwrap();
let schema = Schema::from_str("entity User, Photo;").unwrap();
let entities = PartialEntities::empty();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
None,
schema,
)
.unwrap();
let actions: Vec<_> = policies
.query_action(&request, &entities)
.unwrap()
.collect();
assert_eq!(actions, Vec::new());
}
#[test]
fn permitted_action_error_permit() {
let policies = PolicySet::from_str(&format!("permit(principal, action, resource);permit(principal, action, resource) when {{ {} + 1 == 0 || true }};", i64::MAX)).unwrap();
let schema = Schema::from_str(
"entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
)
.unwrap();
let entities = PartialEntities::empty();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
None,
schema,
)
.unwrap();
let actions: Vec<_> = policies
.query_action(&request, &entities)
.unwrap()
.collect();
assert_eq!(
actions,
vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
);
}
#[test]
fn permitted_action_error_forbid() {
let policies = PolicySet::from_str(&format!("permit(principal, action, resource);forbid(principal, action, resource) when {{ {} + 1 == 0 || true }};", i64::MAX)).unwrap();
let schema = Schema::from_str(
"entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
)
.unwrap();
let entities = PartialEntities::empty();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
None,
schema,
)
.unwrap();
let actions: Vec<_> = policies
.query_action(&request, &entities)
.unwrap()
.collect();
assert_eq!(
actions,
vec![(&r#"Action::"view""#.parse().unwrap(), Some(Decision::Allow))]
);
}
#[test]
fn forbidden_action_error_permit() {
let policies = PolicySet::from_str(&format!(
"permit(principal, action, resource) when {{ {} + 1 == 0 || true }};",
i64::MAX
))
.unwrap();
let schema = Schema::from_str(
"entity User, Photo; action view appliesTo { principal: User, resource: Photo};",
)
.unwrap();
let entities = PartialEntities::empty();
let request = ActionQueryRequest::new(
PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
PartialEntityUid::from_concrete(r#"Photo::"vacation.jpg""#.parse().unwrap()),
None,
schema,
)
.unwrap();
let actions: Vec<_> = policies
.query_action(&request, &entities)
.unwrap()
.collect();
assert_eq!(actions, Vec::new(),);
}
}
#[test]
fn residual_error_to_pst_and_json() {
use cedar_policy_core::pst;
use std::str::FromStr;
let (schema, _) = crate::Schema::from_cedarschema_str(
r#"
entity User = { name: String };
entity Account = { name: String, assignedTo?: User };
action RevealCredentials appliesTo {
principal: [User],
resource: [Account],
context: { flag: Bool },
};
"#,
)
.unwrap();
let policies = crate::PolicySet::from_str(
r#"
permit(
principal is User,
action == Action::"RevealCredentials",
resource is Account
) when {
context.flag &&
resource has assignedTo &&
resource.assignedTo == principal
};
"#,
)
.unwrap();
let entities = crate::Entities::from_json_value(
serde_json::json!([
{
"uid": { "type": "User", "id": "u1" },
"attrs": { "name": "alice" },
"parents": []
},
{
"uid": { "type": "Account", "id": "a1" },
"attrs": { "name": "shared" },
"parents": []
}
]),
Some(&schema),
)
.unwrap();
let partial_entities = crate::PartialEntities::from_concrete(entities, &schema).unwrap();
let request = crate::PartialRequest::new(
crate::PartialEntityUid::from_concrete(r#"User::"u1""#.parse().unwrap()),
r#"Action::"RevealCredentials""#.parse().unwrap(),
crate::PartialEntityUid::from_concrete(r#"Account::"a1""#.parse().unwrap()),
None,
&schema,
)
.unwrap();
let response = policies
.tpe(&request, &partial_entities, &schema)
.expect("tpe should succeed");
let residual_policies: Vec<_> = response.residual_policies().collect();
assert_eq!(
residual_policies.len(),
1,
"decision={:?}, all residuals: {:?}",
response.decision(),
response
.residual_policies()
.map(|p| p.to_string())
.collect::<Vec<_>>()
);
let policy = &residual_policies[0];
let json_res = policy.to_json();
assert!(json_res.is_ok());
assert!(json_res.unwrap().to_string().contains(r#"{"error":[]}"#));
let pst_policy = policy.to_pst().expect("to_pst should succeed");
let clauses = pst_policy.body().clauses();
assert_eq!(clauses.len(), 1);
let expr = match &clauses[0] {
pst::Clause::When(e) => e,
pst::Clause::Unless(_) => panic!("expected when clause"),
};
assert!(
expr.has_error(),
"residual expression should contain an error node"
);
}
mod template_links {
use std::{collections::HashMap, str::FromStr};
use crate::{
pst, Decision, EntityUid, PartialEntities, PartialEntityUid, PartialRequest, Policy,
PolicyId, PolicySet, Schema, SlotId, Template,
};
fn schema() -> Schema {
Schema::from_str(
"entity User { age: Long }; entity Photo; action view appliesTo { principal: User, resource: Photo};",
)
.unwrap()
}
fn template_policy_set() -> PolicySet {
let mut policies = PolicySet::new();
let template = Template::parse(
Some(PolicyId::new("t0").clone()),
"permit(principal == ?principal, action, resource);",
)
.unwrap();
policies.add_template(template).unwrap();
let template = Template::parse(
Some(PolicyId::new("t1").clone()),
"permit(principal, action, resource == ?resource);",
)
.unwrap();
policies.add_template(template).unwrap();
policies
}
fn partial_req() -> PartialRequest {
PartialRequest::new(
PartialEntityUid::from_concrete(r#"User::"alice""#.parse().unwrap()),
r#"Action::"view""#.parse().unwrap(),
PartialEntityUid::new("Photo".parse().unwrap(), None),
None,
&schema(),
)
.unwrap()
}
#[test]
fn concrete_allow() {
let schema = schema();
let mut policies = template_policy_set();
policies
.link(
PolicyId::new("t0"),
PolicyId::new("l"),
HashMap::from([(
SlotId::principal(),
EntityUid::from_str(r#"User::"alice""#).unwrap(),
)]),
)
.unwrap();
let request = partial_req();
let es = PartialEntities::empty();
let response = policies.tpe(&request, &es, &schema).unwrap();
assert_eq!(response.decision(), Some(Decision::Allow));
assert_eq!(
response.reason().unwrap().collect::<Vec<_>>(),
vec![&PolicyId::new("l")]
);
assert_eq!(
response.true_permits().collect::<Vec<_>>(),
vec![&PolicyId::new("l")]
);
}
#[test]
fn templates_no_links_deny() {
let schema = schema();
let policies = template_policy_set();
let request = partial_req();
let es = PartialEntities::empty();
let response = policies.tpe(&request, &es, &schema).unwrap();
assert_eq!(response.decision(), Some(Decision::Deny));
assert_eq!(
response.reason().unwrap().collect::<Vec<_>>(),
Vec::<&PolicyId>::new()
);
}
#[test]
fn concrete_deny() {
let schema = schema();
let mut policies = template_policy_set();
policies
.link(
PolicyId::new("t0"),
PolicyId::new("l"),
HashMap::from([(
SlotId::principal(),
EntityUid::from_str(r#"User::"bob""#).unwrap(),
)]),
)
.unwrap();
let request = partial_req();
let es = PartialEntities::empty();
let response = policies.tpe(&request, &es, &schema).unwrap();
assert_eq!(response.decision(), Some(Decision::Deny));
assert_eq!(
response.reason().unwrap().collect::<Vec<_>>(),
Vec::<&PolicyId>::new()
);
}
#[test]
fn residual() {
let schema = schema();
let mut policies = template_policy_set();
policies
.link(
PolicyId::new("t1"),
PolicyId::new("l"),
HashMap::from([(
SlotId::resource(),
EntityUid::from_str(r#"Photo::"p""#).unwrap(),
)]),
)
.unwrap();
let request = partial_req();
let es = PartialEntities::empty();
let response = policies.tpe(&request, &es, &schema).unwrap();
let expected: pst::Policy = Policy::parse(
Some(PolicyId::new("l")),
r#"permit(principal, action, resource) when { resource == Photo::"p" };"#,
)
.unwrap()
.to_pst()
.unwrap();
let residuals: Vec<_> = response.residual_policies().collect();
assert_eq!(residuals[0].to_pst().unwrap().body(), expected.body());
assert_eq!(response.decision(), None);
assert!(response.reason().is_none());
assert_eq!(residuals.len(), 1);
}
}
}