Initial Dever 0.0.1

This commit is contained in:
Codex committed 2026-11-03 03:32:07 +00:00
commit 81233d098c
1479 files changed
+163782

No files matched your search

+335
View File
@@ -0,0 +1,335 @@
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<api::Config>,
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<Registry> {
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;
}