Files
core/test/rust/runtime/authorization/mod.rs
T
2026-11-03 03:32:07 +00:00

273 lines
10 KiB
Rust

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<i64>,
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<Value>(result: Result<Value, Failure>) {
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();
}