use super::*; use crate::{ api, api::Request, auth::{Identity, Service}, database::{Database, Options}, task::Runtime, }; use std::{ path::PathBuf, sync::{ Arc, atomic::{AtomicU64, Ordering}, }, }; mod mutations; static NEXT: AtomicU64 = AtomicU64::new(0); const ID: [u8; 16] = [1, 144, 0, 0, 0, 0, 112, 0, 128, 0, 0, 0, 0, 0, 0, 1]; const KEY: &str = "capability:news.article.admin.01900000-0000-7000-8000-000000000001"; #[cfg(feature = "postgres")] impl<'database> Tenant<'database> { pub(crate) fn for_test(executor: &'database mut Executor, id: i64) -> Self { Self { executor, id } } } struct Fixture { directory: PathBuf, service: Service, } impl Fixture { fn new() -> Self { let directory = std::env::temp_dir().join(format!( "dever-capability-{}-{}", std::process::id(), NEXT.fetch_add(1, Ordering::Relaxed) )); std::fs::create_dir(&directory).unwrap(); let setting = directory.join("setting.json"); std::fs::write(&setting, r#"{"http":{"host":"127.0.0.1","port":8181},"sites":{"admin":{"path":"admin","hosts":["unit.test"]},"front":{"path":"front","hosts":["unit.test"]}}}"#).unwrap(); let config = api::Config::load(&setting, &["admin", "front"]).unwrap(); let service = Service::new(Arc::new(config), &[], &[]).unwrap(); Self { directory, service } } fn context<'request>( &'request self, request: &'request Request, site: &str, tenant: Option, entry: Option<(&'static str, &'static str)>, ) -> Context<'request> { let (mut context, _) = self.service.prepare(site, request, true, 100).unwrap(); context.identity_for_authorization_test(Identity { id: "test-only".into(), user_id: Some(7), tenant_id: tenant, }); if let Some((component, domain)) = entry { context.bind_entry(component, domain); } context } async fn database(&self, scope: &Scope<'_>, name: &str, initialize: bool) -> Database { let database = Database::open( &self.directory, Options { driver: "sqlite".into(), path: format!("{name}.db"), url: None, tls: None, tenant_directory: None, tenant_database_prefix: None, min_connections: 1, max_connections: 1, max_page_size: 100, wait_timeout_ms: 1000, io_timeout_ms: 1000, }, scope, ) .await .unwrap(); if initialize { let mut executor = database.executor().await.unwrap(); for binding in &crate::authorization::schema::BINDINGS[1..] { executor.initialize(scope, binding.schema).await.unwrap(); } } database } } impl Drop for Fixture { fn drop(&mut self) { std::fs::remove_dir_all(&self.directory).unwrap(); } } fn request(method: &str) -> Request { hyper::Request::builder() .method(method) .uri("/test") .body(Default::default()) .unwrap() } fn denied(result: Result) { assert!(matches!(result, Err(Failure::Access(403)))); } #[test] fn tenant_and_entry_guards_run_before_database_access() { let fixture = Fixture::new(); let post = request("POST"); let get = request("GET"); Runtime::new(Default::default()) .unwrap() .run(async |scope| { let database = fixture.database(scope, "empty", false).await; let mut executor = database.executor().await.unwrap(); let mut tenant = Tenant { executor: &mut executor, id: 1, }; for tenant_id in [None, Some(0), Some(-1), Some(2)] { let context = fixture.context(&post, "admin", tenant_id, Some(("news", "article"))); denied(register_capability(&mut tenant, scope, &context, ID).await); denied(remove_capability(&mut tenant, scope, &context, ID).await); denied(has_protected_roles(&mut tenant, scope, &context, 7).await); denied(capabilities(&mut tenant, scope, &context, false).await); } for (request, entry) in [(&post, None), (&get, Some(("news", "article")))] { let context = fixture.context(request, "admin", Some(1), entry); denied(register_capability(&mut tenant, scope, &context, ID).await); denied(remove_capability(&mut tenant, scope, &context, ID).await); } let (mut anonymous, _) = fixture.service.prepare("admin", &post, true, 100).unwrap(); assert!( anonymous .establish(Identity { id: "u".into(), user_id: Some(7), tenant_id: Some(1) }) .is_err() ); assert!(matches!( has_protected_roles(&mut tenant, scope, &anonymous, 7).await, Err(Failure::Access(401)) )); }) .unwrap(); } #[test] fn capabilities_use_entry_namespace_and_distinct_physical_tenant_stores() { let fixture = Fixture::new(); let post = request("POST"); Runtime::new(Default::default()) .unwrap() .run(async |scope| { let database = fixture.database(scope, "first", true).await; let mut executor = database.executor().await.unwrap(); let mut tenant = Tenant { executor: &mut executor, id: 1, }; let context = fixture.context(&post, "admin", Some(1), Some(("news", "article"))); for _ in 0..2 { assert_eq!( register_capability(&mut tenant, scope, &context, ID) .await .unwrap(), KEY ); } let other = fixture.context(&post, "admin", Some(1), Some(("other", "article"))); let other_key = register_capability(&mut tenant, scope, &other, ID) .await .unwrap(); assert_ne!(other_key, KEY); let front = fixture.context(&post, "front", Some(1), Some(("news", "article"))); let front_key = register_capability(&mut tenant, scope, &front, ID) .await .unwrap(); assert_ne!(front_key, KEY); let listed = capabilities(&mut tenant, scope, &context, false) .await .unwrap(); assert_eq!(listed.len(), 2); assert!(listed.iter().all(|value| value.site == "admin" && value.kind == "capability" && value.method.is_empty())); assert_eq!(listed[0].action, "01900000-0000-7000-8000-000000000001"); assert_eq!( capabilities(&mut tenant, scope, &front, false) .await .unwrap() .len(), 1 ); let second = fixture.database(scope, "second", true).await; let mut second_executor = second.executor().await.unwrap(); let mut second_tenant = Tenant { executor: &mut second_executor, id: 2, }; let second_context = fixture.context(&post, "admin", Some(2), Some(("news", "article"))); assert!( capabilities(&mut second_tenant, scope, &second_context, false) .await .unwrap() .is_empty() ); assert_eq!( register_capability(&mut second_tenant, scope, &second_context, ID) .await .unwrap(), KEY ); remove_capability(&mut tenant, scope, &context, ID) .await .unwrap(); assert_eq!( capabilities(&mut second_tenant, scope, &second_context, false) .await .unwrap() .len(), 1 ); assert_eq!( capabilities(&mut tenant, scope, &other, false) .await .unwrap()[0] .key, other_key ); }) .unwrap(); } #[test] fn protected_roles_span_sites_and_all_permission_roles_follow_the_active_catalog() { let fixture = Fixture::new(); let post = request("POST"); let get = request("GET"); Runtime::new(Default::default()).unwrap().run(async |scope| { let database = fixture.database(scope, "roles", true).await; let mut executor = database.executor().await.unwrap(); executor.initialize(scope, "INSERT INTO _dever_role VALUES ('front','owner','Owner',1,0,1),('admin','all','All',1,1,0); INSERT INTO _dever_user_role VALUES ('front',7,'owner'),('admin',7,'all');").await.unwrap(); let mut tenant = Tenant { executor: &mut executor, id: 1 }; let context = fixture.context(&get, "admin", Some(1), None); assert!(has_protected_roles(&mut tenant, scope, &context, 7).await.unwrap()); assert!(!has_protected_roles(&mut tenant, scope, &context, 8).await.unwrap()); assert!(matches!(has_protected_roles(&mut tenant, scope, &context, 0).await, Err(Failure::Access(400)))); let writer = fixture.context(&post, "admin", Some(1), Some(("news", "article"))); register_capability(&mut tenant, scope, &writer, ID).await.unwrap(); assert_eq!(capabilities(&mut tenant, scope, &context, true).await.unwrap().len(), 1); remove_capability(&mut tenant, scope, &writer, ID).await.unwrap(); assert!(capabilities(&mut tenant, scope, &context, true).await.unwrap().is_empty()); register_capability(&mut tenant, scope, &writer, ID).await.unwrap(); tenant.executor.execute(scope, "UPDATE _dever_role SET enabled=0 WHERE site='admin'", vec![]).await.unwrap(); assert!(capabilities(&mut tenant, scope, &context, true).await.unwrap().is_empty()); }).unwrap(); }