Skip to content

Commit

Permalink
H-4103: Write test for common Policies (#6566)
Browse files Browse the repository at this point in the history
  • Loading branch information
TimDiekmann authored Feb 28, 2025
1 parent 4c1f6f4 commit 6d95a20
Show file tree
Hide file tree
Showing 23 changed files with 1,327 additions and 117 deletions.
2 changes: 2 additions & 0 deletions libs/@local/graph/authorization/schemas/policies.cedarschema
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ namespace HASH {
};

entity EntityType in [Web] {
base_url: String,
version: Long,
};

entity User, Machine in [HASH::Web::Role, HASH::Team::Role, HASH::Web::Team::Role] {
Expand Down
76 changes: 70 additions & 6 deletions libs/@local/graph/authorization/src/policies/context.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
use core::fmt;

use cedar_policy_core::{
ast,
entities::{Entities, TCComputation},
extensions::Extensions,
};
use error_stack::{Report, ResultExt as _};

use super::{Validator, principal::Actor, resource::Resource};
use super::{
PolicyValidator,
principal::{
machine::Machine,
user::User,
web::{Web, WebRole},
},
resource::{EntityResource, EntityTypeResource, Resource},
};

#[derive(Debug, derive_more::Display, derive_more::Error)]
pub enum ContextError {
#[display("transitive closure computation failed")]
TransitiveClosureError,
}

#[derive(Debug, Default)]
#[derive(Default)]
pub struct Context {
entities: Entities,
}

impl fmt::Debug for Context {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.entities, fmt)
}
}

impl Context {
#[must_use]
pub(crate) const fn entities(&self) -> &Entities {
Expand All @@ -31,15 +47,63 @@ pub struct ContextBuilder {
}

impl ContextBuilder {
pub fn add_machine(&mut self, machine: &Machine) {
self.entities.push(machine.to_cedar_entity());
self.entities.push(machine.entity.to_cedar_entity());
}

pub fn add_user(&mut self, user: &User) {
self.entities.push(user.to_cedar_entity());
self.entities.push(user.entity.to_cedar_entity());
}

pub fn add_resource(&mut self, resource: &Resource) {
self.entities.push(resource.to_cedar_entity());
}

pub fn add_entity(&mut self, entity: &EntityResource) {
self.entities.push(entity.to_cedar_entity());
}

pub fn add_entity_type(&mut self, entity_type: &EntityTypeResource) {
self.entities.push(entity_type.to_cedar_entity());
}

pub fn add_web(&mut self, web: &Web) {
self.entities.push(web.to_cedar_entity());
}

pub fn add_web_role(&mut self, web_role: &WebRole) {
self.entities.push(web_role.to_cedar_entity());
}

#[must_use]
pub fn with_actor(mut self, actor: &Actor) -> Self {
self.entities.push(actor.to_cedar_entity());
pub fn with_user(mut self, user: &User) -> Self {
self.add_user(user);
self
}

#[must_use]
pub fn with_machine(mut self, machine: &Machine) -> Self {
self.add_machine(machine);
self
}

#[must_use]
pub fn with_resource(mut self, resource: &Resource) -> Self {
self.entities.push(resource.to_cedar_entity());
self.add_resource(resource);
self
}

#[must_use]
pub fn with_web(mut self, web: &Web) -> Self {
self.add_web(web);
self
}

#[must_use]
pub fn with_web_role(mut self, web_role: &WebRole) -> Self {
self.add_web_role(web_role);
self
}

Expand All @@ -54,7 +118,7 @@ impl ContextBuilder {
Ok(Context {
entities: Entities::from_entities(
self.entities,
Some(&Validator::core_schema()),
Some(&PolicyValidator::core_schema()),
TCComputation::ComputeNow,
Extensions::none(),
)
Expand Down
102 changes: 68 additions & 34 deletions libs/@local/graph/authorization/src/policies/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ use self::{
};
pub use self::{
context::{Context, ContextBuilder, ContextError},
validation::{PolicyValidationError, Validator},
set::{PolicyEvaluationError, PolicySet, PolicySetInsertionError},
validation::{PolicyValidationError, PolicyValidator},
};

#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
Expand Down Expand Up @@ -69,7 +70,7 @@ impl fmt::Display for PolicyId {
}
}

#[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct Policy {
pub id: PolicyId,
Expand All @@ -81,8 +82,14 @@ pub struct Policy {
pub constraints: Option<()>,
}

impl fmt::Debug for Policy {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(fmt, "{}", self.to_cedar_template())
}
}

#[non_exhaustive]
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct RequestContext;

impl RequestContext {
Expand All @@ -97,10 +104,10 @@ impl RequestContext {

#[derive(Debug)]
pub struct Request<'a> {
actor: ActorId,
action: ActionId,
resource: &'a ResourceId<'a>,
context: RequestContext,
pub actor: ActorId,
pub action: ActionId,
pub resource: &'a ResourceId<'a>,
pub context: RequestContext,
}

impl Request<'_> {
Expand All @@ -110,7 +117,7 @@ impl Request<'_> {
(self.action.to_euid(), None),
(self.resource.to_euid(), None),
self.context.to_cedar(),
Some(Validator::schema()),
Some(PolicyValidator::schema()),
Extensions::none(),
)
.expect("Request should be a valid Cedar request")
Expand All @@ -135,6 +142,23 @@ impl Policy {
pub(crate) fn try_from_cedar(
policy: &ast::StaticPolicy,
) -> Result<Self, Report<InvalidPolicy>> {
// let resource_filter = match policy.condition().expr_kind() {
// ast::ExprKind::Lit(ast::Literal::Bool(true)) => None,
// ast::ExprKind::BinaryApp {
// op: ast::BinaryOp::Contains,
// arg1,
// arg2,
// } => {
// let resource_filter =
// if arg1.expr_kind() == ast::ExprKind::Lit(ast::Literal::Bool(true)) {
// Some(arg2)
// } else {
// None
// };
// }
// _ => return Err(InvalidPolicy::InvalidSyntax.into()),
// };

Ok(Self {
id: PolicyId::new(
Uuid::from_str(policy.id().as_ref()).change_context(InvalidPolicy::InvalidId)?,
Expand Down Expand Up @@ -197,9 +221,23 @@ impl Policy {
)),
text,
)
.change_context(InvalidPolicy::InvalidSyntax)?,
.change_context(InvalidPolicy::InvalidSyntax)
.attach_printable_lazy(|| text.to_owned())?,
)
}

/// Parses multiple policies from a string.
///
/// # Errors
///
/// - [`InvalidPolicy::InvalidSyntax`] if the Cedar policy is invalid.
/// - [`InvalidPolicy`] if the Cedar policy cannot be converted to a [`Policy`].
pub fn parse_cedar_policies(text: &str) -> Result<Vec<Self>, Report<InvalidPolicy>> {
text.split_inclusive(';')
.filter(|policy| !policy.trim().is_empty())
.map(|policy| Self::parse_cedar_policy(policy, None))
.collect()
}
}

#[cfg(test)]
Expand All @@ -214,7 +252,7 @@ mod tests {

use super::Policy;
use crate::{
policies::{Validator, set::PolicySet},
policies::{PolicyValidator, set::PolicySet},
test_utils::check_serialization,
};

Expand All @@ -226,9 +264,7 @@ mod tests {
) -> Result<(), Box<dyn Error>> {
check_serialization(policy, value);

let cedar_policy = policy.to_cedar_template();

assert_eq!(cedar_policy.to_string(), cedar_string.as_ref());
assert_eq!(format!("{policy:?}"), cedar_string.as_ref());

let mut policy_set = PolicySet::default();
if policy.principal.has_slot() || policy.resource.has_slot() {
Expand All @@ -238,7 +274,7 @@ mod tests {
policy_set.add_policy(&Policy::try_from_cedar(&static_policy)?)?;
}

Validator.validate_policy_set(&policy_set)?;
PolicyValidator.validate_policy_set(&policy_set)?;

Ok(())
}
Expand All @@ -255,10 +291,10 @@ mod tests {
ActionConstraint, ActionId, ContextBuilder, Effect, PolicyId, PrincipalConstraint,
Request, RequestContext, ResourceConstraint,
principal::{
Actor,
ActorId,
user::{User, UserId, UserPrincipalConstraint},
},
resource::{EntityResource, EntityResourceConstraint, Resource},
resource::{EntityResource, EntityResourceConstraint, ResourceId},
};

#[test]
Expand Down Expand Up @@ -312,26 +348,24 @@ mod tests {
),
)?;

let actor = Actor::User(User {
let user = User {
id: user_id,
roles: Vec::new(),
});
let actor_id = actor.id();

let entity = Resource::Entity(EntityResource {
web_id: OwnedById::new(Uuid::new_v4()),
id: entity_uuid,
entity_type: Cow::Owned(vec![
VersionedUrl::from_str("https://hash.ai/@hash/types/entity-type/user/v/1")
.expect("Invalid entity type URL"),
]),
});
let resource_id = entity.id();

let context = ContextBuilder::default()
.with_actor(&actor)
.with_resource(&entity)
.build()?;
entity: EntityResource {
web_id: OwnedById::new(user_id.into_uuid()),
id: entity_uuid,
entity_type: Cow::Owned(vec![
VersionedUrl::from_str("https://hash.ai/@hash/types/entity-type/user/v/6")?,
VersionedUrl::from_str(
"https://hash.ai/@hash/types/entity-type/actor/v/2",
)?,
]),
},
};
let resource_id = ResourceId::Entity(user.entity.id);
let context = ContextBuilder::default().with_user(&user).build()?;

let actor_id = ActorId::User(user.id);

let mut policy_set = PolicySet::default();
policy_set.add_policy(&policy)?;
Expand Down
29 changes: 1 addition & 28 deletions libs/@local/graph/authorization/src/policies/principal/actor.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
use cedar_policy_core::ast;

use super::{
machine::{Machine, MachineId},
user::{User, UserId},
};
use super::{machine::MachineId, user::UserId};
use crate::policies::cedar::CedarEntityId as _;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
Expand All @@ -20,27 +17,3 @@ impl ActorId {
}
}
}

#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type", rename_all = "camelCase", deny_unknown_fields)]
pub enum Actor {
User(User),
Machine(Machine),
}

impl Actor {
#[must_use]
pub const fn id(&self) -> ActorId {
match self {
Self::User(user) => ActorId::User(user.id),
Self::Machine(machine) => ActorId::Machine(machine.id),
}
}

pub(crate) fn to_cedar_entity(&self) -> ast::Entity {
match self {
Self::User(user) => user.to_entity(),
Self::Machine(machine) => machine.to_entity(),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ use error_stack::Report;
use uuid::Uuid;

use super::{InPrincipalConstraint, TeamPrincipalConstraint, role::RoleId};
use crate::policies::{cedar::CedarEntityId, principal::web::WebPrincipalConstraint};
use crate::policies::{
cedar::CedarEntityId, principal::web::WebPrincipalConstraint, resource::EntityResource,
};

#[derive(
Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
Expand Down Expand Up @@ -70,10 +72,11 @@ impl CedarEntityId for MachineId {
pub struct Machine {
pub id: MachineId,
pub roles: Vec<RoleId>,
pub entity: EntityResource<'static>,
}

impl Machine {
pub(crate) fn to_entity(&self) -> ast::Entity {
pub(crate) fn to_cedar_entity(&self) -> ast::Entity {
ast::Entity::new(
self.id.to_euid(),
iter::empty(),
Expand Down Expand Up @@ -204,7 +207,7 @@ mod tests {
check_deserialization_error::<PrincipalConstraint>(
json!({
"type": "machine",
"machineId": machine_id,
"id": machine_id,
"additional": "unexpected",
}),
"data did not match any variant of untagged enum MachinePrincipalConstraint",
Expand Down
Loading

0 comments on commit 6d95a20

Please sign in to comment.