#![allow(clippy::unwrap_used, clippy::expect_used)]
use modkit_db::migration_runner::run_migrations_for_testing;
use modkit_db::secure::{Db, DbConn, ScopableEntity, ScopeError, secure_insert};
use modkit_db::{ConnectOpts, connect_db};
use modkit_security::{AccessScope, pep_properties};
use sea_orm::Set;
use sea_orm::entity::prelude::*;
use sea_orm_migration::prelude as mig;
use uuid::Uuid;
mod tenant_ent {
use super::*;
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "tenant_insert_test")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub tenant_id: Uuid,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
impl ScopableEntity for tenant_ent::Entity {
fn tenant_col() -> Option<<Self as EntityTrait>::Column> {
Some(tenant_ent::Column::TenantId)
}
fn resource_col() -> Option<<Self as EntityTrait>::Column> {
None
}
fn owner_col() -> Option<<Self as EntityTrait>::Column> {
None
}
fn type_col() -> Option<<Self as EntityTrait>::Column> {
None
}
fn resolve_property(property: &str) -> Option<<Self as EntityTrait>::Column> {
match property {
p if p == pep_properties::OWNER_TENANT_ID => Self::tenant_col(),
_ => None,
}
}
}
mod unrestricted_ent {
use super::*;
#[derive(Debug, Clone, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "unrestricted_insert_test")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i64,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}
}
impl ScopableEntity for unrestricted_ent::Entity {
const IS_UNRESTRICTED: bool = true;
fn tenant_col() -> Option<<Self as EntityTrait>::Column> {
None
}
fn resource_col() -> Option<<Self as EntityTrait>::Column> {
None
}
fn owner_col() -> Option<<Self as EntityTrait>::Column> {
None
}
fn type_col() -> Option<<Self as EntityTrait>::Column> {
None
}
fn resolve_property(_property: &str) -> Option<<Self as EntityTrait>::Column> {
None
}
}
struct CreateSecureInsertTenantValidationTables;
impl mig::MigrationName for CreateSecureInsertTenantValidationTables {
fn name(&self) -> &'static str {
"m001_create_secure_insert_tenant_validation_tables"
}
}
#[async_trait::async_trait]
impl mig::MigrationTrait for CreateSecureInsertTenantValidationTables {
async fn up(&self, manager: &mig::SchemaManager) -> Result<(), mig::DbErr> {
manager
.create_table(
mig::Table::create()
.table(mig::Alias::new("tenant_insert_test"))
.if_not_exists()
.col(
mig::ColumnDef::new(mig::Alias::new("id"))
.big_integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(
mig::ColumnDef::new(mig::Alias::new("tenant_id"))
.uuid()
.not_null(),
)
.col(
mig::ColumnDef::new(mig::Alias::new("name"))
.string()
.not_null(),
)
.to_owned(),
)
.await?;
manager
.create_table(
mig::Table::create()
.table(mig::Alias::new("unrestricted_insert_test"))
.if_not_exists()
.col(
mig::ColumnDef::new(mig::Alias::new("id"))
.big_integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(
mig::ColumnDef::new(mig::Alias::new("name"))
.string()
.not_null(),
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &mig::SchemaManager) -> Result<(), mig::DbErr> {
manager
.drop_table(
mig::Table::drop()
.table(mig::Alias::new("tenant_insert_test"))
.to_owned(),
)
.await?;
manager
.drop_table(
mig::Table::drop()
.table(mig::Alias::new("unrestricted_insert_test"))
.to_owned(),
)
.await
}
}
struct TestDb {
db: Db,
}
impl TestDb {
async fn new() -> Self {
let opts = ConnectOpts {
max_conns: Some(1),
min_conns: Some(1),
..Default::default()
};
let test_id = Uuid::new_v4();
let dsn = format!(
"sqlite:file:memdb_secure_insert_tenant_validation_{test_id}?mode=memory&cache=shared"
);
let db = connect_db(&dsn, opts).await.expect("db connect");
run_migrations_for_testing(
&db,
vec![Box::new(CreateSecureInsertTenantValidationTables)],
)
.await
.expect("migrate");
Self { db }
}
fn conn(&self) -> DbConn<'_> {
self.db.conn().expect("conn")
}
}
async fn setup() -> TestDb {
TestDb::new().await
}
#[tokio::test]
async fn tenant_scoped_insert_allows_tenant_in_scope() {
let test_db = setup().await;
let conn = test_db.conn();
let tenant_a = Uuid::new_v4();
let scope = AccessScope::for_tenants(vec![tenant_a]);
let am = tenant_ent::ActiveModel {
tenant_id: Set(tenant_a),
name: Set("ok".to_owned()),
..Default::default()
};
let _ = secure_insert::<tenant_ent::Entity>(am, &scope, &conn)
.await
.expect("insert ok");
}
#[tokio::test]
async fn tenant_scoped_insert_rejects_tenant_not_in_scope() {
let test_db = setup().await;
let conn = test_db.conn();
let tenant_a = Uuid::new_v4();
let tenant_b = Uuid::new_v4();
let scope = AccessScope::for_tenants(vec![tenant_a]);
let am = tenant_ent::ActiveModel {
tenant_id: Set(tenant_b),
name: Set("nope".to_owned()),
..Default::default()
};
let err = secure_insert::<tenant_ent::Entity>(am, &scope, &conn)
.await
.expect_err("must be rejected");
match err {
ScopeError::Denied(_) => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[tokio::test]
async fn tenant_scoped_insert_rejects_scope_without_tenants() {
let test_db = setup().await;
let conn = test_db.conn();
let tenant_a = Uuid::new_v4();
let scope = AccessScope::default();
let am = tenant_ent::ActiveModel {
tenant_id: Set(tenant_a),
name: Set("nope".to_owned()),
..Default::default()
};
let err = secure_insert::<tenant_ent::Entity>(am, &scope, &conn)
.await
.expect_err("must be rejected");
match err {
ScopeError::Denied(_) => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[tokio::test]
async fn tenant_scoped_insert_requires_tenant_id_in_active_model() {
let test_db = setup().await;
let conn = test_db.conn();
let tenant_a = Uuid::new_v4();
let scope = AccessScope::for_tenants(vec![tenant_a]);
let am = tenant_ent::ActiveModel {
name: Set("missing".to_owned()),
..Default::default()
};
let err = secure_insert::<tenant_ent::Entity>(am, &scope, &conn)
.await
.expect_err("must be rejected");
match err {
ScopeError::Invalid(msg) => assert_eq!(msg, "tenant_id is required"),
other => panic!("unexpected error: {other:?}"),
}
}
#[tokio::test]
async fn unrestricted_entity_insert_does_not_require_tenant_scope_or_tenant_id() {
let test_db = setup().await;
let conn = test_db.conn();
let scope = AccessScope::default();
let am = unrestricted_ent::ActiveModel {
name: Set("ok".to_owned()),
..Default::default()
};
let _ = secure_insert::<unrestricted_ent::Entity>(am, &scope, &conn)
.await
.expect("insert ok");
}