use crate::{ api, api::Request, auth::{Context, Identity, Service}, authorization::{ self as authz, Declaration, Failure, capability::{self, Tenant}, }, database::{Binding, Context as DatabaseContext, Registry, Sql, Storage}, task::Runtime, }; use std::{ path::PathBuf, sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; pub(super) struct Fixture { directory: PathBuf, pub(super) setting: PathBuf, config: Arc, service: Service, request: Request, } impl Fixture { pub(super) fn new() -> Self { let unique = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_nanos(); let name = format!("dever-tenant-auth-{}-{unique}", std::process::id()); let executable = std::env::current_exe().unwrap(); let directory = executable.parent().unwrap().join(&name); std::fs::create_dir(&directory).unwrap(); let setting = directory.join("setting.json"); std::fs::write(&setting, serde_json::json!({ "http":{"host":"127.0.0.1","port":8181}, "sites":{"admin":{"path":"admin","hosts":["unit.test"]}}, "database":{"default":{"type":"sqlite","path":format!("{name}/control.db"),"tenant_directory":format!("{name}/tenants"),"max_connections":1}}, "tenant":{"database":"default","max_pools":4,"idle_timeout_ms":1000} }).to_string()).unwrap(); let config = Arc::new(api::Config::load(&setting, &["admin"]).unwrap()); let service = Service::new(config.clone(), &[], &[]).unwrap(); Self { directory, setting, config, service, request: hyper::Request::builder() .method("POST") .uri("/news/article/admin/create") .body(Default::default()) .unwrap(), } } fn identity(&self, tenant: i64, user: i64) -> Context<'_> { let (mut context, _) = self .service .prepare("admin", &self.request, true, 100) .unwrap(); context.identity_for_authorization_test(Identity { id: "verified-user".into(), user_id: Some(user), tenant_id: Some(tenant), }); context } async fn registry(&self, scope: &crate::task::Scope<'_>) -> Arc { let mut bindings = authz::schema::BINDINGS.to_vec(); bindings.push(Binding { storage: Storage::Tenant, component: "news", database: None, table: "article", schema: Sql::same("CREATE TABLE article(id BIGINT PRIMARY KEY)"), plan: None, references: &[], }); let registry = Registry::load(&self.setting, &bindings, scope) .await .unwrap(); registry.migrate_tenant(scope, 17).await.unwrap(); registry.migrate_tenant(scope, 18).await.unwrap(); let database = DatabaseContext::new(registry.clone()); let mut control = database.control(scope).await.unwrap(); authz::synchronize( &mut control, scope, &self.config, &[Declaration { topic: "admin", component: "news", domain: "article", action: "create", method: "POST", }], ) .await .unwrap(); registry } } impl Drop for Fixture { fn drop(&mut self) { std::fs::remove_dir_all(&self.directory).unwrap(); } } #[test] fn authorization_routes_control_catalog_and_tenant_roles_without_cross_database_joins() { let fixture = Fixture::new(); Runtime::new(Default::default()) .unwrap() .run(async |scope| routing(&fixture, scope).await) .unwrap(); } pub(super) async fn routing(fixture: &Fixture, scope: &crate::task::Scope<'_>) { let registry = fixture.registry(scope).await; let mut first = DatabaseContext::new(registry.clone()); first.select_tenant(scope, 17).await.unwrap(); let mut second = DatabaseContext::new(registry.clone()); second.select_tenant(scope, 18).await.unwrap(); authz::owner(first.on(1, scope).await.unwrap(), scope, "admin", 7) .await .unwrap(); let mut actor = fixture.identity(17, 7); authz::authorize_entry( &mut first, 1, scope, &mut actor, "news", "article", "create", ) .await .unwrap(); assert_eq!(actor.entry_namespace().unwrap(), ("news", "article")); let tenant_tables = first .on(1, scope) .await .unwrap() .query( scope, Sql { sqlite: "SELECT COUNT(*) FROM sqlite_master WHERE name='_dever_permission'", postgres: "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='public' AND table_name='_dever_permission'", }, vec![], 1, |row| row.get::<_, i64>(0), ) .await .unwrap(); assert_eq!(tenant_tables, [0]); let key = capability::register_capability( &mut Tenant::open(&mut first, 1, scope, &actor).await.unwrap(), scope, &actor, [1; 16], ) .await .unwrap(); authz::save( &mut first, 1, scope, &actor, "editor", "Editor", false, std::slice::from_ref(&key), ) .await .unwrap(); authz::grant( first.on(1, scope).await.unwrap(), scope, &actor, 8, "editor", ) .await .unwrap(); let reader = fixture.identity(17, 8); let permissions = authz::permissions(&mut first, 1, scope, &reader, true) .await .unwrap(); assert_eq!(permissions.len(), 1); assert_eq!(permissions[0].key, key); let other = fixture.identity(18, 8); assert!( authz::permissions(&mut second, 1, scope, &other, true) .await .unwrap() .is_empty() ); assert!(matches!( Tenant::open(&mut second, 1, scope, &actor).await, Err(Failure::Access(403)) )); assert_eq!( authz::permissions(&mut first, 1, scope, &actor, true) .await .unwrap() .len(), 2 ); registry .set_component(scope, 17, "news", false) .await .unwrap(); assert!(matches!( authz::authorize_key(&mut first, 1, scope, &actor, "news.article.admin.create").await, Err(Failure::Access(403)) )); assert!( authz::permissions(&mut first, 1, scope, &actor, true) .await .unwrap() .is_empty() ); registry .set_component(scope, 17, "news", true) .await .unwrap(); authz::authorize_key(&mut first, 1, scope, &actor, "news.article.admin.create") .await .unwrap(); drop(first); drop(second); registry.close().await; } #[test] fn capability_role_and_model_changes_share_rollback_and_reject_platform_transactions() { let fixture = Fixture::new(); Runtime::new(Default::default()) .unwrap() .run(async |scope| rollback(&fixture, scope).await) .unwrap(); } pub(super) async fn rollback(fixture: &Fixture, scope: &crate::task::Scope<'_>) { let registry = fixture.registry(scope).await; let mut database = DatabaseContext::new(registry.clone()); database.select_tenant(scope, 17).await.unwrap(); authz::owner(database.on(1, scope).await.unwrap(), scope, "admin", 7) .await .unwrap(); let mut actor = fixture.identity(17, 7); authz::authorize_entry( &mut database, 1, scope, &mut actor, "news", "article", "create", ) .await .unwrap(); let transaction = database.enter(); database .on(5, scope) .await .unwrap() .execute(scope, "INSERT INTO article VALUES (1)", vec![]) .await .unwrap(); let key = capability::register_capability( &mut Tenant::open(&mut database, 1, scope, &actor).await.unwrap(), scope, &actor, [2; 16], ) .await .unwrap(); authz::save( &mut database, 1, scope, &actor, "rolled-back", "Rollback", false, &[key], ) .await .unwrap(); assert_eq!( authz::permissions(&mut database, 1, scope, &actor, false) .await .unwrap() .len(), 2 ); assert!(database.on(0, scope).await.is_err()); database.finish(scope, transaction, false).await.unwrap(); assert_eq!( authz::permissions(&mut database, 1, scope, &actor, false) .await .unwrap() .len(), 1 ); assert!( authz::roles( database.on(1, scope).await.unwrap(), scope, &actor, authz::Selector::Id("rolled-back".into()) ) .await .unwrap() .is_empty() ); assert_eq!( database .on(5, scope) .await .unwrap() .query(scope, "SELECT COUNT(*) FROM article", vec![], 1, |row| row .get::<_, i64>( 0 )) .await .unwrap(), [0] ); let mut missing = DatabaseContext::new(registry.clone()); assert!(missing.select_tenant(scope, 19).await.is_err()); drop(missing); drop(database); registry.close().await; }