use sea_orm::sea_query::{Alias, Query};
use sea_orm::{ColumnTrait, Condition, EntityTrait, sea_query::Expr};
use crate::secure::{AccessScope, ScopableEntity};
use modkit_security::access_scope::{ScopeConstraint, ScopeFilter, ScopeValue, rg_tables};
fn scope_value_to_sea_expr(v: &ScopeValue) -> sea_orm::sea_query::SimpleExpr {
match v {
ScopeValue::Uuid(u) => Expr::value(*u),
ScopeValue::String(s) => Expr::value(s.clone()),
ScopeValue::Int(n) => Expr::value(*n),
ScopeValue::Bool(b) => Expr::value(*b),
}
}
fn scope_values_to_sea_values(values: &[ScopeValue]) -> Vec<sea_orm::Value> {
values
.iter()
.map(|v| match v {
ScopeValue::Uuid(u) => sea_orm::Value::from(*u),
ScopeValue::String(s) => sea_orm::Value::from(s.clone()),
ScopeValue::Int(n) => sea_orm::Value::from(*n),
ScopeValue::Bool(b) => sea_orm::Value::from(*b),
})
.collect()
}
fn deny_all() -> Condition {
Condition::all().add(Expr::value(false))
}
pub fn build_scope_condition<E>(scope: &AccessScope) -> Condition
where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
{
if scope.is_unconstrained() {
return Condition::all();
}
if scope.is_deny_all() {
return deny_all();
}
let compiled: Vec<Condition> = scope
.constraints()
.iter()
.filter_map(build_constraint_condition::<E>)
.collect();
match compiled.len() {
0 => deny_all(),
1 => compiled.into_iter().next().unwrap_or_else(deny_all),
_ => {
let mut or_cond = Condition::any();
for c in compiled {
or_cond = or_cond.add(c);
}
or_cond
}
}
}
fn build_constraint_condition<E>(constraint: &ScopeConstraint) -> Option<Condition>
where
E: ScopableEntity + EntityTrait,
E::Column: ColumnTrait + Copy,
{
if constraint.is_empty() {
return Some(Condition::all());
}
let mut and_cond = Condition::all();
for filter in constraint.filters() {
let col = E::resolve_property(filter.property())?;
match filter {
ScopeFilter::Eq(eq) => {
let expr = scope_value_to_sea_expr(eq.value());
and_cond = and_cond.add(col.into_expr().eq(expr));
}
ScopeFilter::In(inf) => {
let sea_values = scope_values_to_sea_values(inf.values());
and_cond = and_cond.add(col.is_in(sea_values));
}
ScopeFilter::InGroup(gf) => {
let group_values = scope_values_to_sea_values(gf.group_ids());
let subquery = Query::select()
.column(Alias::new(rg_tables::MEMBERSHIP_RESOURCE_ID))
.from(Alias::new(rg_tables::MEMBERSHIP_TABLE))
.and_where(
Expr::col(Alias::new(rg_tables::MEMBERSHIP_GROUP_ID)).is_in(group_values),
)
.to_owned();
and_cond = and_cond.add(col.into_expr().in_subquery(subquery));
}
ScopeFilter::InGroupSubtree(sf) => {
let ancestor_values = scope_values_to_sea_values(sf.ancestor_ids());
let closure_subquery = Query::select()
.column(Alias::new(rg_tables::CLOSURE_DESCENDANT_ID))
.from(Alias::new(rg_tables::CLOSURE_TABLE))
.and_where(
Expr::col(Alias::new(rg_tables::CLOSURE_ANCESTOR_ID))
.is_in(ancestor_values),
)
.to_owned();
let membership_subquery = Query::select()
.column(Alias::new(rg_tables::MEMBERSHIP_RESOURCE_ID))
.from(Alias::new(rg_tables::MEMBERSHIP_TABLE))
.and_where(
Expr::col(Alias::new(rg_tables::MEMBERSHIP_GROUP_ID))
.in_subquery(closure_subquery),
)
.to_owned();
and_cond = and_cond.add(col.into_expr().in_subquery(membership_subquery));
}
}
}
Some(and_cond)
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use super::*;
use modkit_security::access_scope::{ScopeConstraint, ScopeFilter, pep_properties};
#[test]
fn test_deny_all_scope() {
let scope = AccessScope::default();
assert!(scope.is_deny_all());
}
#[test]
fn test_allow_all_scope() {
let scope = AccessScope::allow_all();
assert!(scope.is_unconstrained());
}
#[test]
fn test_tenant_scope_not_empty() {
let tid = uuid::Uuid::new_v4();
let scope = AccessScope::for_tenant(tid);
assert!(!scope.is_deny_all());
assert!(scope.contains_uuid(pep_properties::OWNER_TENANT_ID, tid));
}
#[test]
fn test_or_scope_has_multiple_constraints() {
let t1 = uuid::Uuid::new_v4();
let t2 = uuid::Uuid::new_v4();
let r1 = uuid::Uuid::new_v4();
let scope = AccessScope::from_constraints(vec![
ScopeConstraint::new(vec![
ScopeFilter::in_uuids(pep_properties::OWNER_TENANT_ID, vec![t1]),
ScopeFilter::in_uuids(pep_properties::RESOURCE_ID, vec![r1]),
]),
ScopeConstraint::new(vec![ScopeFilter::in_uuids(
pep_properties::OWNER_TENANT_ID,
vec![t2],
)]),
]);
assert_eq!(scope.constraints().len(), 2);
}
mod custom_prop_entity {
use super::*;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "custom_prop_test")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
pub tenant_id: Uuid,
pub department_id: Uuid,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
impl crate::secure::ScopableEntity for Entity {
fn tenant_col() -> Option<Column> {
Some(Column::TenantId)
}
fn resource_col() -> Option<Column> {
Some(Column::Id)
}
fn owner_col() -> Option<Column> {
None
}
fn type_col() -> Option<Column> {
None
}
fn resolve_property(property: &str) -> Option<Column> {
match property {
p if p == pep_properties::OWNER_TENANT_ID => Some(Column::TenantId),
p if p == pep_properties::RESOURCE_ID => Some(Column::Id),
"department_id" => Some(Column::DepartmentId),
_ => None,
}
}
}
}
#[test]
fn test_custom_property_resolves() {
let dept = uuid::Uuid::new_v4();
let scope =
AccessScope::from_constraints(vec![ScopeConstraint::new(vec![ScopeFilter::in_uuids(
"department_id",
vec![dept],
)])]);
let cond = build_scope_condition::<custom_prop_entity::Entity>(&scope);
let cond_str = format!("{cond:?}");
assert!(
!cond_str.contains("Value(Bool(Some(false)))"),
"Expected a real condition, got deny-all: {cond_str}"
);
}
#[test]
fn test_unknown_property_deny_all() {
let val = uuid::Uuid::new_v4();
let scope =
AccessScope::from_constraints(vec![ScopeConstraint::new(vec![ScopeFilter::in_uuids(
"nonexistent",
vec![val],
)])]);
let cond = build_scope_condition::<custom_prop_entity::Entity>(&scope);
let cond_str = format!("{cond:?}");
assert!(
cond_str.contains("Value(Bool(Some(false)))"),
"Expected deny-all, got: {cond_str}"
);
}
#[test]
fn test_eq_filter_produces_equality_condition() {
let tid = uuid::Uuid::new_v4();
let scope =
AccessScope::from_constraints(vec![ScopeConstraint::new(vec![ScopeFilter::eq(
pep_properties::OWNER_TENANT_ID,
tid,
)])]);
let cond = build_scope_condition::<custom_prop_entity::Entity>(&scope);
let cond_str = format!("{cond:?}");
assert!(
!cond_str.contains("Value(Bool(Some(false)))"),
"Expected a real condition, got deny-all: {cond_str}"
);
}
#[test]
fn test_in_group_filter_produces_subquery_condition() {
let group_id = uuid::Uuid::new_v4();
let scope =
AccessScope::from_constraints(vec![ScopeConstraint::new(vec![ScopeFilter::in_group(
pep_properties::RESOURCE_ID,
vec![ScopeValue::Uuid(group_id)],
)])]);
let cond = build_scope_condition::<custom_prop_entity::Entity>(&scope);
let cond_str = format!("{cond:?}");
assert!(
!cond_str.contains("Value(Bool(Some(false)))"),
"InGroup should produce a real condition, got: {cond_str}"
);
assert!(
cond_str.contains("resource_group_membership"),
"InGroup condition must reference resource_group_membership table, got: {cond_str}"
);
assert!(
cond_str.contains("group_id"),
"InGroup condition must filter by group_id, got: {cond_str}"
);
assert!(
cond_str.contains("resource_id"),
"InGroup condition must join on resource_id, got: {cond_str}"
);
}
#[test]
fn test_in_group_subtree_filter_produces_subquery_condition() {
let ancestor_id = uuid::Uuid::new_v4();
let scope = AccessScope::from_constraints(vec![ScopeConstraint::new(vec![
ScopeFilter::in_group_subtree(
pep_properties::RESOURCE_ID,
vec![ScopeValue::Uuid(ancestor_id)],
),
])]);
let cond = build_scope_condition::<custom_prop_entity::Entity>(&scope);
let cond_str = format!("{cond:?}");
assert!(
!cond_str.contains("Value(Bool(Some(false)))"),
"InGroupSubtree should produce a real condition, got: {cond_str}"
);
assert!(
cond_str.contains("resource_group_membership"),
"InGroupSubtree condition must reference resource_group_membership table, got: {cond_str}"
);
assert!(
cond_str.contains("resource_id"),
"InGroupSubtree condition must join on resource_id, got: {cond_str}"
);
}
#[test]
fn test_tenant_plus_in_group_produces_and_condition() {
let tid = uuid::Uuid::new_v4();
let gid = uuid::Uuid::new_v4();
let scope = AccessScope::from_constraints(vec![ScopeConstraint::new(vec![
ScopeFilter::in_uuids(pep_properties::OWNER_TENANT_ID, vec![tid]),
ScopeFilter::in_group(pep_properties::RESOURCE_ID, vec![ScopeValue::Uuid(gid)]),
])]);
let cond = build_scope_condition::<custom_prop_entity::Entity>(&scope);
let cond_str = format!("{cond:?}");
assert!(
!cond_str.contains("Value(Bool(Some(false)))"),
"Combined tenant+group should produce a real condition, got: {cond_str}"
);
}
#[test]
fn test_standard_plus_custom_scope() {
let tid = uuid::Uuid::new_v4();
let dept = uuid::Uuid::new_v4();
let scope = AccessScope::from_constraints(vec![ScopeConstraint::new(vec![
ScopeFilter::in_uuids(pep_properties::OWNER_TENANT_ID, vec![tid]),
ScopeFilter::in_uuids("department_id", vec![dept]),
])]);
let cond = build_scope_condition::<custom_prop_entity::Entity>(&scope);
let cond_str = format!("{cond:?}");
assert!(
!cond_str.contains("Value(Bool(Some(false)))"),
"Expected a real condition, got deny-all: {cond_str}"
);
}
}