use super::*;
use crate::constraints::{EqPredicate, InPredicate};
use crate::models::EvaluationResponseContext;
use modkit_security::pep_properties;
use serde_json::json;
use uuid::Uuid;
fn uuid(s: &str) -> Uuid {
Uuid::parse_str(s).unwrap()
}
fn jid(s: &str) -> serde_json::Value {
json!(s)
}
const T1: &str = "11111111-1111-1111-1111-111111111111";
const T2: &str = "22222222-2222-2222-2222-222222222222";
const R1: &str = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
const DEFAULT_PROPS: &[&str] = &[pep_properties::OWNER_TENANT_ID, pep_properties::RESOURCE_ID];
#[test]
fn no_require_constraints_empty_returns_allow_all() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext::default(),
};
let scope = compile_to_access_scope(&response, false, DEFAULT_PROPS).unwrap();
assert!(scope.is_unconstrained());
}
#[test]
fn no_require_constraints_with_constraints_compiles_them() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![Predicate::Eq(EqPredicate {
property: pep_properties::OWNER_TENANT_ID.to_owned(),
value: jid(T1),
})],
}],
..Default::default()
},
};
let scope = compile_to_access_scope(&response, false, DEFAULT_PROPS).unwrap();
assert!(!scope.is_unconstrained());
assert_eq!(
scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
&[uuid(T1)]
);
}
#[test]
fn decision_true_require_constraints_empty_returns_error() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext::default(),
};
let result = compile_to_access_scope(&response, true, DEFAULT_PROPS);
assert!(matches!(
result,
Err(ConstraintCompileError::ConstraintsRequiredButAbsent)
));
}
#[test]
fn single_tenant_eq_constraint() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![Predicate::Eq(EqPredicate {
property: pep_properties::OWNER_TENANT_ID.to_owned(),
value: jid(T1),
})],
}],
..Default::default()
},
};
let scope = compile_to_access_scope(&response, true, DEFAULT_PROPS).unwrap();
assert_eq!(
scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
&[uuid(T1)]
);
assert!(
scope
.all_uuid_values_for(pep_properties::RESOURCE_ID)
.is_empty()
);
let filter = &scope.constraints()[0].filters()[0];
assert!(matches!(filter, ScopeFilter::Eq(_)));
}
#[test]
fn multiple_tenants_in_constraint() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![Predicate::In(InPredicate {
property: pep_properties::OWNER_TENANT_ID.to_owned(),
values: vec![jid(T1), jid(T2)],
})],
}],
..Default::default()
},
};
let scope = compile_to_access_scope(&response, true, DEFAULT_PROPS).unwrap();
assert_eq!(
scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
&[uuid(T1), uuid(T2)]
);
}
#[test]
fn resource_id_eq_constraint() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![Predicate::Eq(EqPredicate {
property: pep_properties::RESOURCE_ID.to_owned(),
value: jid(R1),
})],
}],
..Default::default()
},
};
let scope = compile_to_access_scope(&response, true, DEFAULT_PROPS).unwrap();
assert!(
scope
.all_uuid_values_for(pep_properties::OWNER_TENANT_ID)
.is_empty()
);
assert_eq!(
scope.all_uuid_values_for(pep_properties::RESOURCE_ID),
&[uuid(R1)]
);
let filter = &scope.constraints()[0].filters()[0];
assert!(matches!(filter, ScopeFilter::Eq(_)));
}
#[test]
fn multiple_constraints_produce_or_scope() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![
Constraint {
predicates: vec![Predicate::In(InPredicate {
property: pep_properties::OWNER_TENANT_ID.to_owned(),
values: vec![jid(T1)],
})],
},
Constraint {
predicates: vec![Predicate::In(InPredicate {
property: pep_properties::OWNER_TENANT_ID.to_owned(),
values: vec![jid(T2)],
})],
},
],
..Default::default()
},
};
let scope = compile_to_access_scope(&response, true, DEFAULT_PROPS).unwrap();
assert_eq!(scope.constraints().len(), 2);
assert!(scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uuid(T1)));
assert!(scope.contains_uuid(pep_properties::OWNER_TENANT_ID, uuid(T2)));
}
#[test]
fn unknown_predicate_fails_constraint() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![Predicate::Eq(EqPredicate {
property: "unknown_property".to_owned(),
value: jid(T1),
})],
}],
..Default::default()
},
};
let result = compile_to_access_scope(&response, true, DEFAULT_PROPS);
assert!(matches!(
result,
Err(ConstraintCompileError::AllConstraintsFailed { .. })
));
}
#[test]
fn mixed_known_and_unknown_constraints() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![
Constraint {
predicates: vec![Predicate::Eq(EqPredicate {
property: "group_id".to_owned(),
value: jid(T1),
})],
},
Constraint {
predicates: vec![Predicate::In(InPredicate {
property: pep_properties::OWNER_TENANT_ID.to_owned(),
values: vec![jid(T2)],
})],
},
],
..Default::default()
},
};
let scope = compile_to_access_scope(&response, true, DEFAULT_PROPS).unwrap();
assert_eq!(
scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
&[uuid(T2)]
);
}
#[test]
fn both_tenant_and_resource_in_single_constraint() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![
Predicate::In(InPredicate {
property: pep_properties::OWNER_TENANT_ID.to_owned(),
values: vec![jid(T1)],
}),
Predicate::Eq(EqPredicate {
property: pep_properties::RESOURCE_ID.to_owned(),
value: jid(R1),
}),
],
}],
..Default::default()
},
};
let scope = compile_to_access_scope(&response, true, DEFAULT_PROPS).unwrap();
assert_eq!(scope.constraints().len(), 1);
assert_eq!(
scope.all_uuid_values_for(pep_properties::OWNER_TENANT_ID),
&[uuid(T1)]
);
assert_eq!(
scope.all_uuid_values_for(pep_properties::RESOURCE_ID),
&[uuid(R1)]
);
}
#[test]
fn mixed_shape_constraints_produce_or_scope() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![
Constraint {
predicates: vec![
Predicate::In(InPredicate {
property: pep_properties::OWNER_TENANT_ID.to_owned(),
values: vec![jid(T1)],
}),
Predicate::Eq(EqPredicate {
property: pep_properties::RESOURCE_ID.to_owned(),
value: jid(R1),
}),
],
},
Constraint {
predicates: vec![Predicate::In(InPredicate {
property: pep_properties::OWNER_TENANT_ID.to_owned(),
values: vec![jid(T2)],
})],
},
],
..Default::default()
},
};
let scope = compile_to_access_scope(&response, true, DEFAULT_PROPS).unwrap();
assert_eq!(scope.constraints().len(), 2);
assert_eq!(scope.constraints()[0].filters().len(), 2);
assert_eq!(scope.constraints()[1].filters().len(), 1);
}
#[test]
fn in_group_predicate_compiles_to_in_group_filter() {
use crate::constraints::InGroupPredicate;
let g1 = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa";
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![Predicate::InGroup(InGroupPredicate {
property: pep_properties::RESOURCE_ID.to_owned(),
group_ids: vec![jid(g1)],
})],
}],
..Default::default()
},
};
let scope = compile_to_access_scope(&response, true, DEFAULT_PROPS).unwrap();
assert_eq!(scope.constraints().len(), 1);
let filter = &scope.constraints()[0].filters()[0];
assert!(
matches!(filter, ScopeFilter::InGroup(_)),
"expected InGroup filter, got: {filter:?}"
);
assert_eq!(filter.property(), pep_properties::RESOURCE_ID);
}
#[test]
fn in_group_subtree_predicate_compiles_to_subtree_filter() {
use crate::constraints::InGroupSubtreePredicate;
let ancestor = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb";
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![Predicate::InGroupSubtree(InGroupSubtreePredicate {
property: pep_properties::RESOURCE_ID.to_owned(),
ancestor_ids: vec![jid(ancestor)],
})],
}],
..Default::default()
},
};
let scope = compile_to_access_scope(&response, true, DEFAULT_PROPS).unwrap();
assert_eq!(scope.constraints().len(), 1);
let filter = &scope.constraints()[0].filters()[0];
assert!(
matches!(filter, ScopeFilter::InGroupSubtree(_)),
"expected InGroupSubtree filter, got: {filter:?}"
);
}
#[test]
fn tenant_plus_in_group_in_single_constraint() {
use crate::constraints::InGroupPredicate;
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![
Predicate::In(InPredicate {
property: pep_properties::OWNER_TENANT_ID.to_owned(),
values: vec![jid(T1)],
}),
Predicate::InGroup(InGroupPredicate {
property: pep_properties::RESOURCE_ID.to_owned(),
group_ids: vec![jid(R1)],
}),
],
}],
..Default::default()
},
};
let scope = compile_to_access_scope(&response, true, DEFAULT_PROPS).unwrap();
assert_eq!(scope.constraints().len(), 1);
assert_eq!(scope.constraints()[0].filters().len(), 2);
}
#[test]
fn supported_properties_validation() {
let limited_props: &[&str] = &[pep_properties::OWNER_TENANT_ID];
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![Predicate::Eq(EqPredicate {
property: pep_properties::RESOURCE_ID.to_owned(),
value: jid(R1),
})],
}],
..Default::default()
},
};
let result = compile_to_access_scope(&response, true, limited_props);
assert!(matches!(
result,
Err(ConstraintCompileError::AllConstraintsFailed { .. })
));
}
#[test]
fn empty_in_values_fails_constraint() {
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![Predicate::In(InPredicate {
property: pep_properties::OWNER_TENANT_ID.to_owned(),
values: vec![],
})],
}],
..Default::default()
},
};
let result = compile_to_access_scope(&response, true, DEFAULT_PROPS);
assert!(
matches!(
result,
Err(ConstraintCompileError::AllConstraintsFailed { .. })
),
"empty In values must fail-closed, got: {result:?}"
);
}
#[test]
fn empty_in_group_ids_fails_constraint() {
use crate::constraints::InGroupPredicate;
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![Predicate::InGroup(InGroupPredicate {
property: pep_properties::RESOURCE_ID.to_owned(),
group_ids: vec![],
})],
}],
..Default::default()
},
};
let result = compile_to_access_scope(&response, true, DEFAULT_PROPS);
assert!(
matches!(
result,
Err(ConstraintCompileError::AllConstraintsFailed { .. })
),
"empty InGroup group_ids must fail-closed, got: {result:?}"
);
}
#[test]
fn empty_in_group_subtree_ancestor_ids_fails_constraint() {
use crate::constraints::InGroupSubtreePredicate;
let response = EvaluationResponse {
decision: true,
context: EvaluationResponseContext {
constraints: vec![Constraint {
predicates: vec![Predicate::InGroupSubtree(InGroupSubtreePredicate {
property: pep_properties::RESOURCE_ID.to_owned(),
ancestor_ids: vec![],
})],
}],
..Default::default()
},
};
let result = compile_to_access_scope(&response, true, DEFAULT_PROPS);
assert!(
matches!(
result,
Err(ConstraintCompileError::AllConstraintsFailed { .. })
),
"empty InGroupSubtree ancestor_ids must fail-closed, got: {result:?}"
);
}