-
Notifications
You must be signed in to change notification settings - Fork 159
Add functions for inspecting TPE determining policies #2401
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
6e677ed
8197a5a
0276383
2da907c
a1b7f39
85ce1f9
a1b0e6a
feccd1a
ce2ef49
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -59,8 +59,8 @@ impl ResidualPolicy { | |
| } | ||
|
|
||
| /// Get the [`PolicyID`] | ||
| pub fn get_policy_id(&self) -> PolicyID { | ||
| self.policy.id().clone() | ||
| pub fn get_policy_id(&self) -> &PolicyID { | ||
| self.policy.id() | ||
| } | ||
|
john-h-kastner-aws marked this conversation as resolved.
|
||
|
|
||
| /// All literal uids referenced by this residual | ||
|
|
@@ -88,13 +88,13 @@ pub struct Response<'a> { | |
| decision: Option<Decision>, | ||
| residuals: HashMap<PolicyID, ResidualPolicy>, | ||
| // All of the [`Effect::Permit`] policies that were satisfied | ||
| satisfied_permits: HashSet<PolicyID>, | ||
| true_permits: HashSet<PolicyID>, | ||
| // All of the [`Effect::Permit`] policies that were not satisfied | ||
| false_permits: HashSet<PolicyID>, | ||
| // All of the [`Effect::Permit`] policies that evaluated to a residual | ||
| residual_permits: HashSet<PolicyID>, | ||
| // All of the [`Effect::Forbid`] policies that were satisfied | ||
| satisfied_forbids: HashSet<PolicyID>, | ||
| true_forbids: HashSet<PolicyID>, | ||
| // All of the [`Effect::Forbid`] policies that were not satisfied | ||
| false_forbids: HashSet<PolicyID>, | ||
| // All of the [`Effect::Forbid`] policies that evaluated to a residual | ||
|
|
@@ -117,10 +117,10 @@ impl<'a> Response<'a> { | |
| schema: &'a ValidatorSchema, | ||
| ) -> Self { | ||
| let mut residual_map = HashMap::new(); | ||
| let mut satisfied_permits = HashSet::new(); | ||
| let mut true_permits = HashSet::new(); | ||
| let mut false_permits = HashSet::new(); | ||
| let mut residual_permits = HashSet::new(); | ||
| let mut satisfied_forbids = HashSet::new(); | ||
| let mut true_forbids = HashSet::new(); | ||
| let mut false_forbids = HashSet::new(); | ||
| let mut residual_forbids = HashSet::new(); | ||
| for rp in residuals { | ||
|
|
@@ -130,28 +130,28 @@ impl<'a> Response<'a> { | |
| match rp.get_effect() { | ||
| Effect::Forbid => { | ||
| if r.is_true() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: feels like this could be a good candidate to have an enum / match statement for in the future.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could write this as a match on the residual, but I don't think I see how it would make the code any nicer |
||
| satisfied_forbids.insert(id); | ||
| true_forbids.insert(id.clone()); | ||
| } else if r.is_false() || r.is_error() { | ||
| false_forbids.insert(id); | ||
| false_forbids.insert(id.clone()); | ||
| } else { | ||
| residual_forbids.insert(id); | ||
| residual_forbids.insert(id.clone()); | ||
| } | ||
| } | ||
| Effect::Permit => { | ||
| if r.is_true() { | ||
| satisfied_permits.insert(id); | ||
| true_permits.insert(id.clone()); | ||
| } else if r.is_false() || r.is_error() { | ||
| false_permits.insert(id); | ||
| false_permits.insert(id.clone()); | ||
| } else { | ||
| residual_permits.insert(id); | ||
| residual_permits.insert(id.clone()); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let decision = match ( | ||
| !satisfied_forbids.is_empty(), | ||
| !satisfied_permits.is_empty(), | ||
| !true_forbids.is_empty(), | ||
| !true_permits.is_empty(), | ||
| !residual_permits.is_empty(), | ||
| !residual_forbids.is_empty(), | ||
| ) { | ||
|
|
@@ -170,10 +170,10 @@ impl<'a> Response<'a> { | |
| Self { | ||
| decision, | ||
| residuals: residual_map, | ||
| satisfied_permits, | ||
| true_permits, | ||
| false_permits, | ||
| residual_permits, | ||
| satisfied_forbids, | ||
| true_forbids, | ||
| false_forbids, | ||
| residual_forbids, | ||
| request, | ||
|
|
@@ -188,7 +188,7 @@ impl<'a> Response<'a> { | |
| clippy::unwrap_used, | ||
| reason = "we know that the policy ids are in the residuals map" | ||
| )] | ||
| self.satisfied_permits | ||
| self.true_permits | ||
| .iter() | ||
| .map(|id| self.residuals.get(id).unwrap()) | ||
| } | ||
|
|
@@ -199,7 +199,7 @@ impl<'a> Response<'a> { | |
| clippy::unwrap_used, | ||
| reason = "we know that the policy ids are in the residuals map" | ||
| )] | ||
| self.satisfied_forbids | ||
| self.true_forbids | ||
| .iter() | ||
| .map(|id| self.residuals.get(id).unwrap()) | ||
| } | ||
|
|
@@ -258,6 +258,14 @@ impl<'a> Response<'a> { | |
| self.decision | ||
| } | ||
|
|
||
| /// Get the determining policies for the authorization decision | ||
| pub fn reason(&self) -> Option<impl Iterator<Item = &PolicyID>> { | ||
| match self.decision? { | ||
| Decision::Allow => Some(self.true_permits.iter()), | ||
| Decision::Deny => Some(self.true_forbids.iter()), | ||
| } | ||
| } | ||
|
|
||
| /// Perform reauthorization | ||
| pub fn reauthorize( | ||
| &self, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,8 +32,9 @@ use smol_str::SmolStr; | |
|
|
||
| use crate::{ | ||
| api, tpe_err, Authorizer, Context, Entities, Entity, EntityId, EntityTypeName, EntityUid, | ||
| PartialEntityError, PartialRequestCreationError, PermissionQueryError, Policy, PolicySet, | ||
| Request, RequestValidationError, RestrictedExpression, Schema, TpeReauthorizationError, | ||
| PartialEntityError, PartialRequestCreationError, PermissionQueryError, Policy, PolicyId, | ||
| PolicySet, Request, RequestValidationError, RestrictedExpression, Schema, | ||
| TpeReauthorizationError, | ||
| }; | ||
|
|
||
| /// A partial [`EntityUid`]. | ||
|
|
@@ -384,6 +385,54 @@ impl TpeResponse<'_> { | |
| self.0.decision() | ||
| } | ||
|
|
||
| /// Get the determining policies for the partial authorization decision. | ||
| /// These are a subset of the determining policies for any subsequent | ||
| /// concrete reauthorization. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea is that if you were to do concrete authorization, there might be more determining policies that aren't represented here. If you want to take some action based on which policies apply to a request, this might be important.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. expanded comments substantially. let me know if it makes sense
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe |
||
| /// | ||
| /// When [`TpeResponse::decision`] returns a concrete allow or deny, the | ||
| /// determining policies returned by this function are the satisfied permits | ||
| /// or forbids respectively. | ||
|
john-h-kastner-aws marked this conversation as resolved.
Outdated
|
||
| /// | ||
| /// If partial authorization does not reach a decision, then this function | ||
| /// returns `None`. It's reasonable to treat this response as "no known | ||
| /// determining policies", in which case you can call this function as | ||
| /// `response.reason().into_iter().flatten()`. | ||
| pub fn reason(&self) -> Option<impl Iterator<Item = &PolicyId>> { | ||
| Some(self.0.reason()?.map(PolicyId::ref_cast)) | ||
| } | ||
|
|
||
|
john-h-kastner-aws marked this conversation as resolved.
|
||
| /// Get the permit policies that are concretely satisfied by the partial request. | ||
|
luxas marked this conversation as resolved.
|
||
| /// | ||
| /// To properly interpret the ids returned from this function you need to | ||
| /// consider them in the context of [`TpeResponse::decision`]: | ||
| /// * For a concrete `Allow` decision, these are a subset of the concrete | ||
| /// determining policies and are exactly the policies returned by | ||
| /// [`TpeResponse::reason`]. | ||
| /// * For a concrete `Deny` decision, these are not determining policies. The | ||
| /// iterator may be empty if no permits were satisfied, or it may contain | ||
| /// satisfied permits which have been overridden by at least one satisfied | ||
| /// forbid policy. | ||
| /// * For an unknown decision, these will be a subset of the determining | ||
| /// policies _if_ the eventual concrete authorization decision is `Allow`, | ||
| /// but they may still be overridden by any non-trivial residual forbid | ||
| /// policy. | ||
| pub fn true_permits(&self) -> impl Iterator<Item = &PolicyId> { | ||
| self.0 | ||
| .satisfied_permits() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. one missing getter rename, or don't we want to update all of these at the same time? |
||
| .map(|rp| PolicyId::ref_cast(rp.get_policy_id())) | ||
| } | ||
|
john-h-kastner-aws marked this conversation as resolved.
|
||
|
|
||
| /// Get the forbid policies that are concretely satisfied by the partial request. | ||
| /// | ||
| /// Presence of any satisfied forbids guarantees that they are exactly the | ||
| /// policies returned by [`TpeResponse::reason`] and that [`TpeResponse::decision`] | ||
| /// must return a concrete `Deny`. | ||
| pub fn true_forbids(&self) -> impl Iterator<Item = &PolicyId> { | ||
| self.0 | ||
| .satisfied_forbids() | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. seems like one missing getter rename |
||
| .map(|rp| PolicyId::ref_cast(rp.get_policy_id())) | ||
| } | ||
|
|
||
| /// Perform reauthorization | ||
| pub fn reauthorize( | ||
| &self, | ||
|
|
@@ -1259,6 +1308,7 @@ unless | |
| ); | ||
|
|
||
| assert_eq!(response.decision(), None); | ||
| assert!(response.reason().is_none()); | ||
|
|
||
| let request = Request::new( | ||
| EntityUid::from_type_name_and_id( | ||
|
|
@@ -2115,8 +2165,8 @@ when { principal in resource.admins }; | |
| use itertools::Itertools; | ||
|
|
||
| use crate::{ | ||
| Context, Entities, PartialEntities, PartialEntityUid, PartialRequest, PolicySet, | ||
| PrincipalQueryRequest, ResourceQueryRequest, Schema, | ||
| Context, Entities, PartialEntities, PartialEntityUid, PartialRequest, PolicyId, | ||
| PolicySet, PrincipalQueryRequest, ResourceQueryRequest, Schema, | ||
| }; | ||
| use std::{i64, str::FromStr}; | ||
|
|
||
|
|
@@ -2153,6 +2203,10 @@ when { principal in resource.admins }; | |
| .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] | ||
|
|
@@ -2214,6 +2268,14 @@ when { principal in resource.admins }; | |
| .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] | ||
|
|
@@ -2278,6 +2340,10 @@ when { principal in resource.admins }; | |
| .tpe(&req, &partial_entities, &schema) | ||
| .unwrap(); | ||
| assert_eq!(response.decision(), Some(Decision::Deny)); | ||
| assert_eq!( | ||
| response.reason().unwrap().collect::<Vec<_>>(), | ||
| Vec::<&PolicyId>::new() | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
|
|
@@ -2345,6 +2411,10 @@ when { principal in resource.admins }; | |
| .tpe(&req, &partial_entities, &schema) | ||
| .unwrap(); | ||
| assert_eq!(response.decision(), Some(Decision::Deny)); | ||
| assert_eq!( | ||
| response.reason().unwrap().collect::<Vec<_>>(), | ||
| Vec::<&PolicyId>::new() | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
|
|
@@ -2877,6 +2947,14 @@ when { principal in resource.admins }; | |
| 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] | ||
|
|
@@ -2889,6 +2967,10 @@ when { principal in resource.admins }; | |
| 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] | ||
|
|
@@ -2911,6 +2993,10 @@ when { principal in resource.admins }; | |
| 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] | ||
|
|
@@ -2943,6 +3029,7 @@ when { principal in resource.admins }; | |
| let residuals: Vec<_> = response.nontrivial_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); | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.