#![allow(unused_imports, dead_code)] pub mod common; pub use common::{TestContext, bakery_chain::*, setup::*}; use sea_orm::{ ColumnTrait, ConnectionTrait, DbConn, DbErr, EntityName, EntityTrait, IntoActiveModel, NotSet, QueryFilter, Set, TransactionTrait, entity::prelude::ChronoUtc, }; #[sea_orm_macros::test] #[cfg(feature = "rbac")] async fn main() { let ctx = TestContext::new("bakery_chain_rbac_tests").await; create_tables(&ctx.db).await.unwrap(); sea_orm::rbac::schema::create_tables(&ctx.db, Default::default()) .await .unwrap(); rbac_setup(&ctx.db).await.unwrap(); crud_tests(&ctx.db).await.unwrap(); ctx.delete().await; } #[cfg(feature = "rbac")] async fn rbac_setup(db: &DbConn) -> Result<(), DbErr> { use sea_orm::rbac::{RbacAddRoleHierarchy, RbacContext}; let mut context = RbacContext::load(db).await?; let tables = [ baker::Entity.table_name(), bakery::Entity.table_name(), cake::Entity.table_name(), cakes_bakers::Entity.table_name(), customer::Entity.table_name(), lineitem::Entity.table_name(), order::Entity.table_name(), "*", // WILDCARD ]; context.add_tables(db, &tables).await?; context.add_crud_permissions(db).await?; context .add_roles(db, &["admin", "manager", "public"]) .await?; context .assign_user_role(db, &[(1, "admin"), (2, "manager"), (3, "public")]) .await?; // public can select everything context .add_role_permissions(db, "public", &["select"], &["*"]) .await?; // manager can create / update everything except bakery context .add_role_permissions( db, "manager", &["insert", "update"], &tables .iter() .cloned() .filter(|t| !matches!(*t, "bakery" | "*")) .collect::>(), ) .await?; // manager can delete order context .add_role_permissions(db, "manager", &["delete"], &["order", "lineitem"]) .await?; // admin can do anything, in addition to public / manager context .add_role_permissions(db, "admin", &["delete"], &["*"]) .await?; // add permissions to bakery which manager doesn't have context .add_role_permissions(db, "admin", &["insert", "update"], &["bakery"]) .await?; context .add_role_hierarchy( db, &[ RbacAddRoleHierarchy { super_role: "admin", role: "manager", }, RbacAddRoleHierarchy { super_role: "manager", role: "public", }, ], ) .await?; Ok(()) } #[cfg(feature = "rbac")] async fn crud_tests(db: &DbConn) -> Result<(), DbErr> { use sea_orm::{RestrictedConnection, RestrictedTransaction, rbac::RbacUserId}; db.load_rbac().await?; let admin = RbacUserId(1); let manager = RbacUserId(2); let public = RbacUserId(3); async fn admin_create_bakery(db: RestrictedConnection) -> Result<(), DbErr> { // only admin can create bakery let seaside_bakery = bakery::ActiveModel { name: Set("SeaSide Bakery".to_owned()), profit_margin: Set(10.2), ..Default::default() }; let res = Bakery::insert(seaside_bakery).exec(&db).await?; let bakery: Option = Bakery::find_by_id(res.last_insert_id).one(&db).await?; assert_eq!(bakery.unwrap().name, "SeaSide Bakery"); Ok(()) } admin_create_bakery(db.restricted_for(admin)?).await?; // manager / public can't create bakery for user in [manager, public] { assert!(matches!( Bakery::insert(bakery::ActiveModel::default()) .exec(&db.restricted_for(user)?) .await, Err(DbErr::AccessDenied { .. }) )); let txn = db.restricted_for(user)?.begin().await?; assert!(matches!( Bakery::insert(bakery::ActiveModel::default()) .exec(&txn) .await, Err(DbErr::AccessDenied { .. }) )); } // anyone can read bakery for user_id in [1, 2, 3] { let db = db.restricted_for(RbacUserId(user_id))?; let bakery = Bakery::find().one(&db).await?.unwrap(); assert_eq!(bakery.name, "SeaSide Bakery"); } // manager can create cake / baker { let db = db.restricted_for(manager)?; cake::Entity::insert(cake::ActiveModel { name: Set("Cheesecake".to_owned()), price: Set(2.into()), bakery_id: Set(Some(1)), gluten_free: Set(false), ..Default::default() }) .exec(&db) .await .expect("insert succeeds"); db.transaction_async::<_, _, DbErr>(async |txn| { cake::Entity::insert(cake::ActiveModel { name: Set("Chocolate".to_owned()), price: Set(3.into()), bakery_id: Set(Some(1)), gluten_free: Set(true), ..Default::default() }) .exec(txn) .await?; Ok(()) }) .await .expect("insert succeeds"); let txn: RestrictedTransaction = db.begin().await?; baker::Entity::insert(baker::ActiveModel { name: Set("Master Baker".to_owned()), contact_details: Set(Default::default()), bakery_id: Set(Some(1)), ..Default::default() }) .exec(&txn) .await .expect("insert succeeds"); txn.commit().await?; } assert_eq!(cake::Entity::find().all(db).await?.len(), 2); // anyone can read cake for user_id in [1, 2, 3] { let db = db.restricted_for(RbacUserId(user_id))?; let cake = cake::Entity::find().one(&db).await?.unwrap(); assert_eq!(cake.name, "Cheesecake"); } // admin can create customer { let db = db.restricted_for(admin)?; customer::Entity::insert(customer::ActiveModel { id: Set(11), name: Set("Alice".to_owned()), notes: Set(None), }) .exec(&db) .await?; customer::Entity::insert(customer::ActiveModel { id: Set(12), name: Set("Bob".to_owned()), notes: Set(None), }) .exec(&db) .await?; } // manager can create / delete order { let public_db = db.restricted_for(public)?; let db = db.restricted_for(manager)?; order::Entity::insert(order::ActiveModel { id: Set(101), total: Set(10.into()), bakery_id: Set(1), customer_id: Set(11), placed_at: Set(ChronoUtc::now()), }) .exec(&db) .await?; lineitem::Entity::insert(lineitem::ActiveModel { id: NotSet, price: Set(2.into()), quantity: Set(2), order_id: Set(101), cake_id: Set(1), }) .exec(&db) .await?; let to_insert = lineitem::ActiveModel { id: NotSet, price: Set(3.into()), quantity: Set(3), order_id: Set(101), cake_id: Set(2), }; let lineitem_id = if db.support_returning() { lineitem::Entity::insert(to_insert) .exec_with_returning(&db) .await? .id } else { lineitem::Entity::insert(to_insert) .exec(&db) .await? .last_insert_id }; let order_with_items = order::Entity::find() .find_with_related(lineitem::Entity) .all(&public_db) .await?; assert_eq!(order_with_items[0].1.len(), 2); lineitem::Entity::delete_many() .filter(lineitem::Column::Id.eq(lineitem_id)) .exec(&db) .await?; // reject; of course assert!(matches!( lineitem::Entity::delete_many() .filter(lineitem::Column::Id.eq(lineitem_id)) .exec(&public_db) .await, Err(DbErr::AccessDenied { .. }) )); // only 1 line item left let order_with_items = order::Entity::find() .find_with_related(lineitem::Entity) .all(&public_db) .await?; assert_eq!(order_with_items[0].1.len(), 1); } // manager can update order { use sea_orm::ActiveModelTrait; let db = db.restricted_for(manager)?; let lineitem = lineitem::Entity::find_by_id(1).one(&db).await?.unwrap(); assert_eq!(lineitem.quantity, 2); let mut lineitem = lineitem.into_active_model(); lineitem.quantity = Set(3); let lineitem = lineitem.save(&db).await?; assert_eq!(lineitem.quantity.unwrap(), 3); } // only admin can delete customer { use sea_orm::ModelTrait; let db = db.restricted_for(admin)?; let bob = customer::Entity::find_by_id(12).one(&db).await?.unwrap(); assert_eq!(bob.name, "Bob"); bob.delete(&db).await?; assert!(customer::Entity::find_by_id(12).one(&db).await?.is_none()); } Ok(()) }