use std::collections::HashMap;
use either::Either;
use smol_str::SmolStr;
use std::sync::Arc;
use super::{
err::{ConcretizationError, ReauthorizationError},
Annotations, AuthorizationError, Authorizer, Context, Decision, Effect, EntityUIDEntry, Expr,
Policy, PolicySet, PolicySetError, Request, Response, Value,
};
use crate::{ast::PolicyID, entities::Entities, evaluator::EvaluationError};
type PolicyComponents<'a> = (Effect, &'a PolicyID, &'a Arc<Expr>, &'a Arc<Annotations>);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ErrorState {
NoError,
Error,
}
#[derive(Debug, Clone)]
pub struct PartialResponse {
pub satisfied_permits: HashMap<PolicyID, Arc<Annotations>>,
pub false_permits: HashMap<PolicyID, (ErrorState, Arc<Annotations>)>,
pub residual_permits: HashMap<PolicyID, (Arc<Expr>, Arc<Annotations>)>,
pub satisfied_forbids: HashMap<PolicyID, Arc<Annotations>>,
pub false_forbids: HashMap<PolicyID, (ErrorState, Arc<Annotations>)>,
pub residual_forbids: HashMap<PolicyID, (Arc<Expr>, Arc<Annotations>)>,
pub errors: Vec<AuthorizationError>,
true_expr: Arc<Expr>,
false_expr: Arc<Expr>,
request: Arc<Request>,
}
impl PartialResponse {
#[allow(clippy::too_many_arguments)]
pub fn new(
true_permits: impl IntoIterator<Item = (PolicyID, Arc<Annotations>)>,
false_permits: impl IntoIterator<Item = (PolicyID, (ErrorState, Arc<Annotations>))>,
residual_permits: impl IntoIterator<Item = (PolicyID, (Arc<Expr>, Arc<Annotations>))>,
true_forbids: impl IntoIterator<Item = (PolicyID, Arc<Annotations>)>,
false_forbids: impl IntoIterator<Item = (PolicyID, (ErrorState, Arc<Annotations>))>,
residual_forbids: impl IntoIterator<Item = (PolicyID, (Arc<Expr>, Arc<Annotations>))>,
errors: impl IntoIterator<Item = AuthorizationError>,
request: Arc<Request>,
) -> Self {
Self {
satisfied_permits: true_permits.into_iter().collect(),
false_permits: false_permits.into_iter().collect(),
residual_permits: residual_permits.into_iter().collect(),
satisfied_forbids: true_forbids.into_iter().collect(),
false_forbids: false_forbids.into_iter().collect(),
residual_forbids: residual_forbids.into_iter().collect(),
errors: errors.into_iter().collect(),
true_expr: Arc::new(Expr::val(true)),
false_expr: Arc::new(Expr::val(false)),
request,
}
}
pub fn concretize(self) -> Response {
self.into()
}
pub fn decision(&self) -> Option<Decision> {
match (
!self.satisfied_forbids.is_empty(),
!self.satisfied_permits.is_empty(),
!self.residual_permits.is_empty(),
!self.residual_forbids.is_empty(),
) {
(true, _, _, _) => Some(Decision::Deny),
(_, false, false, _) => Some(Decision::Deny),
(false, _, _, true) => None,
(false, false, true, false) => None,
(false, true, _, false) => Some(Decision::Allow),
}
}
fn definitely_satisfied_permits(&self) -> impl Iterator<Item = Policy> + '_ {
self.satisfied_permits.iter().map(|(id, annotations)| {
construct_policy((Effect::Permit, id, &self.true_expr, annotations))
})
}
fn definitely_satisfied_forbids(&self) -> impl Iterator<Item = Policy> + '_ {
self.satisfied_forbids.iter().map(|(id, annotations)| {
construct_policy((Effect::Forbid, id, &self.true_expr, annotations))
})
}
pub fn definitely_satisfied(&self) -> impl Iterator<Item = Policy> + '_ {
self.definitely_satisfied_permits()
.chain(self.definitely_satisfied_forbids())
}
pub fn definitely_errored(&self) -> impl Iterator<Item = &PolicyID> {
self.false_permits
.iter()
.chain(self.false_forbids.iter())
.filter_map(did_error)
}
pub fn may_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
if self.satisfied_forbids.is_empty() {
Either::Left(
self.definitely_satisfied_permits()
.chain(self.residual_permits())
.chain(self.residual_forbids()),
)
} else {
Either::Right(
self.definitely_satisfied_forbids()
.chain(self.residual_forbids()),
)
}
}
fn residual_permits(&self) -> impl Iterator<Item = Policy> + '_ {
self.residual_permits
.iter()
.map(|(id, (expr, annotations))| {
construct_policy((Effect::Permit, id, expr, annotations))
})
}
fn residual_forbids(&self) -> impl Iterator<Item = Policy> + '_ {
self.residual_forbids
.iter()
.map(|(id, (expr, annotations))| {
construct_policy((Effect::Forbid, id, expr, annotations))
})
}
pub fn must_be_determining(&self) -> impl Iterator<Item = Policy> + '_ {
if self.satisfied_forbids.is_empty() && self.residual_forbids.is_empty() {
Either::Left(self.definitely_satisfied_permits())
} else {
Either::Right(self.definitely_satisfied_forbids())
}
}
pub fn nontrivial_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
self.nontrival_permits().chain(self.nontrival_forbids())
}
pub fn nontrivial_residual_ids(&self) -> impl Iterator<Item = &PolicyID> {
self.residual_permits
.keys()
.chain(self.residual_forbids.keys())
}
fn nontrival_permits(&self) -> impl Iterator<Item = Policy> + '_ {
self.residual_permits
.iter()
.map(|(id, (expr, annotations))| {
construct_policy((Effect::Permit, id, expr, annotations))
})
}
pub fn nontrival_forbids(&self) -> impl Iterator<Item = Policy> + '_ {
self.residual_forbids
.iter()
.map(|(id, (expr, annotations))| {
construct_policy((Effect::Forbid, id, expr, annotations))
})
}
pub fn all_residuals(&'_ self) -> impl Iterator<Item = Policy> + '_ {
self.all_permit_residuals()
.chain(self.all_forbid_residuals())
.map(construct_policy)
}
fn all_permit_residuals(&'_ self) -> impl Iterator<Item = PolicyComponents<'_>> {
let trues = self
.satisfied_permits
.iter()
.map(|(id, a)| (id, (&self.true_expr, a)));
let falses = self
.false_permits
.iter()
.map(|(id, (_, a))| (id, (&self.false_expr, a)));
let nontrivial = self
.residual_permits
.iter()
.map(|(id, (r, a))| (id, (r, a)));
trues
.chain(falses)
.chain(nontrivial)
.map(|(id, (r, a))| (Effect::Permit, id, r, a))
}
fn all_forbid_residuals(&'_ self) -> impl Iterator<Item = PolicyComponents<'_>> {
let trues = self
.satisfied_forbids
.iter()
.map(|(id, a)| (id, (&self.true_expr, a)));
let falses = self
.false_forbids
.iter()
.map(|(id, (_, a))| (id, (&self.false_expr, a)));
let nontrivial = self
.residual_forbids
.iter()
.map(|(id, (r, a))| (id, (r, a)));
trues
.chain(falses)
.chain(nontrivial)
.map(|(id, (r, a))| (Effect::Forbid, id, r, a))
}
pub fn get(&self, id: &PolicyID) -> Option<Policy> {
self.get_permit(id).or_else(|| self.get_forbid(id))
}
fn get_permit(&self, id: &PolicyID) -> Option<Policy> {
self.residual_permits
.get(id)
.map(|(a, b)| (a, b))
.or_else(|| self.satisfied_permits.get(id).map(|a| (&self.true_expr, a)))
.or_else(|| {
self.false_permits
.get(id)
.map(|(_, a)| (&self.false_expr, a))
})
.map(|(expr, a)| construct_policy((Effect::Permit, id, expr, a)))
}
fn get_forbid(&self, id: &PolicyID) -> Option<Policy> {
self.residual_forbids
.get(id)
.map(|(a, b)| (a, b))
.or_else(|| self.satisfied_forbids.get(id).map(|a| (&self.true_expr, a)))
.or_else(|| {
self.false_forbids
.get(id)
.map(|(_, a)| (&self.false_expr, a))
})
.map(|(expr, a)| construct_policy((Effect::Forbid, id, expr, a)))
}
pub fn reauthorize(
&self,
mapping: &HashMap<SmolStr, Value>,
auth: &Authorizer,
es: &Entities,
) -> Result<Self, ReauthorizationError> {
let policyset = self.all_policies(mapping)?;
let new_request = self.concretize_request(mapping)?;
Ok(auth.is_authorized_core(new_request, &policyset, es))
}
fn all_policies(&self, mapping: &HashMap<SmolStr, Value>) -> Result<PolicySet, PolicySetError> {
let mapper = map_unknowns(mapping);
PolicySet::try_from_iter(
self.all_permit_residuals()
.chain(self.all_forbid_residuals())
.map(mapper),
)
}
fn concretize_request(
&self,
mapping: &HashMap<SmolStr, Value>,
) -> Result<Request, ConcretizationError> {
let mut context = self.request.context.clone();
let principal = self.request.principal().concretize("principal", mapping)?;
let action = self.request.action.concretize("action", mapping)?;
let resource = self.request.resource.concretize("resource", mapping)?;
if let Some((key, val)) = mapping.get_key_value("context") {
if let Ok(attrs) = val.get_as_record() {
match self.request.context() {
Some(ctx) => {
return Err(ConcretizationError::VarConfictError {
id: key.to_owned(),
existing_value: ctx.clone().into(),
given_value: val.clone(),
});
}
None => context = Some(Context::Value(attrs.clone())),
}
} else {
return Err(ConcretizationError::ValueError {
id: key.to_owned(),
expected_type: "record",
given_value: val.to_owned(),
});
}
}
context = context
.map(|context| context.substitute(mapping))
.transpose()?;
Ok(Request {
principal,
action,
resource,
context,
})
}
fn errors(self) -> impl Iterator<Item = AuthorizationError> {
self.residual_forbids
.into_iter()
.chain(self.residual_permits)
.map(
|(id, (expr, _))| AuthorizationError::PolicyEvaluationError {
id,
error: EvaluationError::non_value(expr.as_ref().clone()),
},
)
.chain(self.errors)
.collect::<Vec<_>>()
.into_iter()
}
}
impl EntityUIDEntry {
fn concretize(
&self,
key: &str,
mapping: &HashMap<SmolStr, Value>,
) -> Result<Self, ConcretizationError> {
if let Some(val) = mapping.get(key) {
if let Ok(uid) = val.get_as_entity() {
match self {
EntityUIDEntry::Known { euid, .. } => {
Err(ConcretizationError::VarConfictError {
id: key.into(),
existing_value: euid.as_ref().clone().into(),
given_value: val.clone(),
})
}
EntityUIDEntry::Unknown { ty: None, .. } => {
Ok(EntityUIDEntry::known(uid.clone(), None))
}
EntityUIDEntry::Unknown {
ty: Some(type_of_unknown),
..
} => {
if type_of_unknown == uid.entity_type() {
Ok(EntityUIDEntry::known(uid.clone(), None))
} else {
Err(ConcretizationError::EntityTypeConfictError {
id: key.into(),
existing_value: type_of_unknown.clone(),
given_value: val.to_owned(),
})
}
}
}
} else {
Err(ConcretizationError::ValueError {
id: key.into(),
expected_type: "entity",
given_value: val.to_owned(),
})
}
} else {
Ok(self.clone())
}
}
}
impl From<PartialResponse> for Response {
fn from(p: PartialResponse) -> Self {
let decision = if !p.satisfied_permits.is_empty() && p.satisfied_forbids.is_empty() {
Decision::Allow
} else {
Decision::Deny
};
Response::new(
decision,
p.must_be_determining().map(|p| p.id().clone()).collect(),
p.errors().collect(),
)
}
}
fn construct_policy((effect, id, expr, annotations): PolicyComponents<'_>) -> Policy {
Policy::from_when_clause_annos(
effect,
expr.clone(),
id.clone(),
expr.source_loc().cloned(),
(*annotations).clone(),
)
}
fn map_unknowns<'a>(
mapping: &'a HashMap<SmolStr, Value>,
) -> impl Fn(PolicyComponents<'a>) -> Policy {
|(effect, id, expr, annotations)| {
Policy::from_when_clause_annos(
effect,
Arc::new(expr.substitute(mapping)),
id.clone(),
expr.source_loc().cloned(),
annotations.clone(),
)
}
}
fn did_error<'a>(
(id, (state, _)): (&'a PolicyID, &'_ (ErrorState, Arc<Annotations>)),
) -> Option<&'a PolicyID> {
match *state {
ErrorState::NoError => None,
ErrorState::Error => Some(id),
}
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod test {
use std::{
collections::HashSet,
iter::{empty, once},
};
#[derive(Debug, Default)]
struct SlowSet<T> {
contents: Vec<T>,
}
impl<T: PartialEq> SlowSet<T> {
pub fn from(iter: impl IntoIterator<Item = T>) -> Self {
let mut contents = vec![];
for item in iter.into_iter() {
if !contents.contains(&item) {
contents.push(item)
}
}
Self { contents }
}
pub fn len(&self) -> usize {
self.contents.len()
}
pub fn contains(&self, item: &T) -> bool {
self.contents.contains(item)
}
}
impl<T: PartialEq> PartialEq for SlowSet<T> {
fn eq(&self, rhs: &Self) -> bool {
if self.len() == rhs.len() {
self.contents.iter().all(|item| rhs.contains(item))
} else {
false
}
}
}
impl<T: PartialEq> FromIterator<T> for SlowSet<T> {
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = T>,
{
Self::from(iter)
}
}
use crate::{
authorizer::{
ActionConstraint, EntityUID, PrincipalConstraint, ResourceConstraint, RestrictedExpr,
Unknown,
},
extensions::Extensions,
parser::parse_policyset,
FromNormalizedStr,
};
use super::*;
#[test]
fn sanity_check() {
let empty_annotations: Arc<Annotations> = Arc::default();
let one_plus_two = Arc::new(Expr::add(Expr::val(1), Expr::val(2)));
let three_plus_four = Arc::new(Expr::add(Expr::val(3), Expr::val(4)));
let a = once((PolicyID::from_string("a"), empty_annotations.clone()));
let bc = [
(
PolicyID::from_string("b"),
(ErrorState::Error, empty_annotations.clone()),
),
(
PolicyID::from_string("c"),
(ErrorState::NoError, empty_annotations.clone()),
),
];
let d = once((
PolicyID::from_string("d"),
(one_plus_two.clone(), empty_annotations.clone()),
));
let e = once((PolicyID::from_string("e"), empty_annotations.clone()));
let fg = [
(
PolicyID::from_string("f"),
(ErrorState::Error, empty_annotations.clone()),
),
(
PolicyID::from_string("g"),
(ErrorState::NoError, empty_annotations.clone()),
),
];
let h = once((
PolicyID::from_string("h"),
(three_plus_four.clone(), empty_annotations),
));
let errs = empty();
let pr = PartialResponse::new(
a,
bc,
d,
e,
fg,
h,
errs,
Arc::new(Request::new_unchecked(
EntityUIDEntry::unknown(),
EntityUIDEntry::unknown(),
EntityUIDEntry::unknown(),
Some(Context::empty()),
)),
);
let a = Policy::from_when_clause(
Effect::Permit,
Expr::val(true),
PolicyID::from_string("a"),
None,
);
let b = Policy::from_when_clause(
Effect::Permit,
Expr::val(false),
PolicyID::from_string("b"),
None,
);
let c = Policy::from_when_clause(
Effect::Permit,
Expr::val(false),
PolicyID::from_string("c"),
None,
);
let d = Policy::from_when_clause_annos(
Effect::Permit,
one_plus_two,
PolicyID::from_string("d"),
None,
Arc::default(),
);
let e = Policy::from_when_clause(
Effect::Forbid,
Expr::val(true),
PolicyID::from_string("e"),
None,
);
let f = Policy::from_when_clause(
Effect::Forbid,
Expr::val(false),
PolicyID::from_string("f"),
None,
);
let g = Policy::from_when_clause(
Effect::Forbid,
Expr::val(false),
PolicyID::from_string("g"),
None,
);
let h = Policy::from_when_clause_annos(
Effect::Forbid,
three_plus_four,
PolicyID::from_string("h"),
None,
Arc::default(),
);
assert_eq!(
pr.definitely_satisfied_permits().collect::<SlowSet<_>>(),
SlowSet::from([a.clone()])
);
assert_eq!(
pr.definitely_satisfied_forbids().collect::<SlowSet<_>>(),
SlowSet::from([e.clone()])
);
assert_eq!(
pr.definitely_satisfied().collect::<SlowSet<_>>(),
SlowSet::from([a.clone(), e.clone()])
);
assert_eq!(
pr.definitely_errored().collect::<HashSet<_>>(),
HashSet::from([&PolicyID::from_string("b"), &PolicyID::from_string("f")])
);
assert_eq!(
pr.may_be_determining().collect::<SlowSet<_>>(),
SlowSet::from([e.clone(), h.clone()])
);
assert_eq!(
pr.must_be_determining().collect::<SlowSet<_>>(),
SlowSet::from([e.clone()])
);
assert_eq!(pr.nontrivial_residuals().count(), 2);
assert_eq!(
pr.nontrivial_residuals().collect::<SlowSet<_>>(),
SlowSet::from([d.clone(), h.clone()])
);
assert_eq!(
pr.all_residuals().collect::<SlowSet<_>>(),
SlowSet::from([&a, &b, &c, &d, &e, &f, &g, &h].into_iter().cloned())
);
assert_eq!(
pr.nontrivial_residual_ids().collect::<HashSet<_>>(),
HashSet::from([&PolicyID::from_string("d"), &PolicyID::from_string("h")])
);
assert_eq!(pr.get(&PolicyID::from_string("a")), Some(a));
assert_eq!(pr.get(&PolicyID::from_string("b")), Some(b));
assert_eq!(pr.get(&PolicyID::from_string("c")), Some(c));
assert_eq!(pr.get(&PolicyID::from_string("d")), Some(d));
assert_eq!(pr.get(&PolicyID::from_string("e")), Some(e));
assert_eq!(pr.get(&PolicyID::from_string("f")), Some(f));
assert_eq!(pr.get(&PolicyID::from_string("g")), Some(g));
assert_eq!(pr.get(&PolicyID::from_string("h")), Some(h));
assert_eq!(pr.get(&PolicyID::from_string("i")), None);
}
#[test]
fn build_policies_trivial_permit() {
let e = Arc::new(Expr::add(Expr::val(1), Expr::val(2)));
let id = PolicyID::from_string("foo");
let p = construct_policy((Effect::Permit, &id, &e, &Arc::default()));
assert_eq!(p.effect(), Effect::Permit);
assert!(p.annotations().next().is_none());
assert_eq!(p.action_constraint(), &ActionConstraint::Any);
assert_eq!(p.principal_constraint(), PrincipalConstraint::any());
assert_eq!(p.resource_constraint(), ResourceConstraint::any());
assert_eq!(p.id(), &id);
assert_eq!(p.non_scope_constraints(), e.as_ref());
}
#[test]
fn build_policies_trivial_forbid() {
let e = Arc::new(Expr::add(Expr::val(1), Expr::val(2)));
let id = PolicyID::from_string("foo");
let p = construct_policy((Effect::Forbid, &id, &e, &Arc::default()));
assert_eq!(p.effect(), Effect::Forbid);
assert!(p.annotations().next().is_none());
assert_eq!(p.action_constraint(), &ActionConstraint::Any);
assert_eq!(p.principal_constraint(), PrincipalConstraint::any());
assert_eq!(p.resource_constraint(), ResourceConstraint::any());
assert_eq!(p.id(), &id);
assert_eq!(p.non_scope_constraints(), e.as_ref());
}
#[test]
fn did_error_error() {
assert_eq!(
did_error((
&PolicyID::from_string("foo"),
&(ErrorState::Error, Arc::default())
)),
Some(&PolicyID::from_string("foo"))
);
}
#[test]
fn did_error_noerror() {
assert_eq!(
did_error((
&PolicyID::from_string("foo"),
&(ErrorState::NoError, Arc::default())
)),
None,
);
}
#[test]
fn reauthorize() {
let policies = parse_policyset(
r#"
permit(principal, action, resource) when {
principal == NS::"a" && resource == NS::"b"
};
forbid(principal, action, resource) when {
context.b
};
"#,
)
.unwrap();
let context_unknown = Context::from_pairs(
std::iter::once((
"b".into(),
RestrictedExpr::unknown(Unknown::new_untyped("b")),
)),
Extensions::all_available(),
)
.unwrap();
let partial_request = Request {
principal: EntityUIDEntry::known(r#"NS::"a""#.parse().unwrap(), None),
action: EntityUIDEntry::unknown(),
resource: EntityUIDEntry::unknown(),
context: Some(context_unknown),
};
let entities = Entities::new();
let authorizer = Authorizer::new();
let partial_response = authorizer.is_authorized_core(partial_request, &policies, &entities);
let response_with_concrete_resource = partial_response
.reauthorize(
&HashMap::from_iter(std::iter::once((
"resource".into(),
EntityUID::from_normalized_str(r#"NS::"b""#).unwrap().into(),
))),
&authorizer,
&entities,
)
.unwrap();
assert_eq!(
response_with_concrete_resource
.definitely_satisfied()
.next()
.unwrap()
.effect(),
Effect::Permit
);
let response_with_concrete_context_attr = response_with_concrete_resource
.reauthorize(
&HashMap::from_iter(std::iter::once(("b".into(), true.into()))),
&authorizer,
&entities,
)
.unwrap();
assert_eq!(
response_with_concrete_context_attr.decision(),
Some(Decision::Deny)
);
}
}