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;
}
@@ -0,0 +1,7 @@
use super::{Context, Identity};
impl Context<'_> {
pub(crate) fn identity_for_authorization_test(&mut self, identity: Identity) {
self.identity = Some(identity);
}
}
+272
View File
@@ -0,0 +1,272 @@
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();
}
@@ -0,0 +1,89 @@
use super::*;
#[test]
fn capability_directory_includes_permissions_beyond_the_model_page_limit() {
let fixture = Fixture::new();
let post = request("POST");
Runtime::new(Default::default())
.unwrap()
.run(async |scope| {
let database = fixture.database(scope, "pages", true).await;
let mut executor = database.executor().await.unwrap();
crate::authorization::owner(&mut executor, scope, "admin", 7)
.await
.unwrap();
let mut tenant = Tenant {
executor: &mut executor,
id: 1,
};
let context = fixture.context(&post, "admin", Some(1), Some(("news", "article")));
for value in 1u128..=101 {
register_capability(&mut tenant, scope, &context, value.to_be_bytes())
.await
.unwrap();
}
for current in [false, true] {
let entries = capabilities(&mut tenant, scope, &context, current)
.await
.unwrap();
assert_eq!(entries.len(), 101);
assert!(entries.windows(2).all(|pair| pair[0].key < pair[1].key));
}
})
.unwrap();
}
#[test]
fn removal_is_atomic_clears_all_grants_and_reregistration_does_not_restore_them() {
let fixture = Fixture::new();
let post = request("POST");
Runtime::new(Default::default()).unwrap().run(async |scope| {
let database = fixture.database(scope, "removal", 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")));
register_capability(&mut tenant, scope, &context, ID).await.unwrap();
tenant.executor.initialize(scope, "INSERT INTO _dever_role VALUES ('admin','reader','Reader',0,1,0),('admin','second','Second',0,1,0); INSERT INTO _dever_user_role VALUES ('admin',7,'reader');").await.unwrap();
for role in ["reader", "second"] {
tenant.executor.execute(scope, "INSERT INTO _dever_role_permission VALUES ('admin',$1,$2)", vec![Parameter::Text(role.into()), Parameter::Text(KEY.into())]).await.unwrap();
}
assert_eq!(capabilities(&mut tenant, scope, &context, true).await.unwrap().len(), 1);
let owner = tenant.executor.begin(scope).await.unwrap();
remove_capability(&mut tenant, scope, &context, ID).await.unwrap();
assert!(capabilities(&mut tenant, scope, &context, true).await.unwrap().is_empty());
tenant.executor.finish(scope, owner, false).await.unwrap();
assert_eq!(capabilities(&mut tenant, scope, &context, true).await.unwrap().len(), 1);
remove_capability(&mut tenant, scope, &context, ID).await.unwrap();
remove_capability(&mut tenant, scope, &context, ID).await.unwrap();
let counts = tenant.executor.query(scope, "SELECT COUNT(*) FROM _dever_role_permission", vec![], 1, |row| row.get::<_, i64>(0)).await.unwrap();
assert_eq!(counts, [0]);
register_capability(&mut tenant, scope, &context, ID).await.unwrap();
assert!(capabilities(&mut tenant, scope, &context, true).await.unwrap().is_empty());
assert_eq!(capabilities(&mut tenant, scope, &context, false).await.unwrap().len(), 1);
}).unwrap();
}
#[test]
fn registration_and_models_share_rollback_and_failed_removal_keeps_the_catalog_active() {
let fixture = Fixture::new();
let post = request("POST");
Runtime::new(Default::default()).unwrap().run(async |scope| {
let database = fixture.database(scope, "rollback", true).await;
let mut executor = database.executor().await.unwrap();
executor.initialize(scope, "CREATE TABLE article(id BIGINT PRIMARY KEY);").await.unwrap();
let mut tenant = Tenant { executor: &mut executor, id: 1 };
let context = fixture.context(&post, "admin", Some(1), Some(("news", "article")));
let owner = tenant.executor.begin(scope).await.unwrap();
tenant.executor.execute(scope, "INSERT INTO article VALUES (1)", vec![]).await.unwrap();
register_capability(&mut tenant, scope, &context, ID).await.unwrap();
tenant.executor.finish(scope, owner, false).await.unwrap();
assert!(capabilities(&mut tenant, scope, &context, false).await.unwrap().is_empty());
assert_eq!(tenant.executor.query(scope, "SELECT COUNT(*) FROM article", vec![], 1, |row| row.get::<_, i64>(0)).await.unwrap(), [0]);
register_capability(&mut tenant, scope, &context, ID).await.unwrap();
tenant.executor.initialize(scope, "CREATE TRIGGER reject_removal BEFORE DELETE ON _dever_role_permission BEGIN SELECT RAISE(ABORT, 'test-only failure'); END; INSERT INTO _dever_role VALUES ('admin','reader','Reader',0,1,0);").await.unwrap();
tenant.executor.execute(scope, "INSERT INTO _dever_role_permission VALUES ('admin','reader',$1)", vec![Parameter::Text(KEY.into())]).await.unwrap();
assert!(matches!(remove_capability(&mut tenant, scope, &context, ID).await, Err(Failure::Database(_))));
assert_eq!(capabilities(&mut tenant, scope, &context, false).await.unwrap().len(), 1);
assert_eq!(tenant.executor.query(scope, "SELECT COUNT(*) FROM _dever_role_permission", vec![], 1, |row| row.get::<_, i64>(0)).await.unwrap(), [1]);
}).unwrap();
}
@@ -0,0 +1,87 @@
//! 行锁测试固定使用已借用连接;目录路由的真实 Context 验证由租户集成用例覆盖。
pub(super) use crate::authorization::{
disable, grant, owner, remove, revoke, roles, schema, synchronize,
};
use crate::{
auth::Context,
authorization::{Failure, Permission, access, capability, catalog, roles as storage},
database::Executor,
task::Scope,
};
pub(super) async fn permissions(
executor: &mut Executor,
scope: &Scope<'_>,
context: &Context<'_>,
current: bool,
) -> Result<Vec<Permission>, Failure> {
context.id()?;
let mut entries = catalog::read(executor, scope, context.site(), None).await?;
if let Some(id) = context.verified_tenant() {
let mut tenant = capability::Tenant::for_test(executor, id);
entries.extend(capability::capabilities(&mut tenant, scope, context, false).await?);
}
if current {
let Some(user) = context.user_id(None)? else {
return Ok(Vec::new());
};
access::filter(executor, scope, context, user, &mut entries).await?;
}
entries.sort_unstable_by(|left, right| left.key.cmp(&right.key));
Ok(entries)
}
pub(super) async fn authorize_key(
executor: &mut Executor,
scope: &Scope<'_>,
context: &Context<'_>,
key: &str,
) -> Result<(), Failure> {
context.id()?;
if !catalog::read(executor, scope, context.site(), None)
.await?
.iter()
.any(|entry| entry.key == key)
{
return Err(Failure::Access(403));
}
storage::granted(executor, scope, context, key).await
}
pub(super) async fn authorize_entry(
executor: &mut Executor,
scope: &Scope<'_>,
context: &mut Context<'_>,
component: &'static str,
domain: &'static str,
action: &str,
) -> Result<(), Failure> {
authorize_key(
executor,
scope,
context,
&format!("{component}.{domain}.{}.{action}", context.site()),
)
.await?;
context.bind_entry(component, domain);
Ok(())
}
pub(super) async fn save(
executor: &mut Executor,
scope: &Scope<'_>,
context: &Context<'_>,
id: &str,
name: &str,
all: bool,
keys: &[String],
) -> Result<(), Failure> {
context.id()?;
let catalog = catalog::read(executor, scope, context.site(), None)
.await?
.into_iter()
.map(|entry| entry.key)
.collect();
storage::save(executor, scope, context, catalog, id, name, all, keys).await
}
@@ -0,0 +1,222 @@
use super::*;
use crate::authorization::capability::{self, Tenant};
const ID: [u8; 16] = [1, 144, 0, 0, 0, 0, 112, 0, 128, 0, 0, 0, 0, 0, 0, 1];
fn context(case: &Case) -> Context<'_> {
let mut context = case.context("admin", Some(USER));
context.identity_for_authorization_test(Identity {
id: "verified-user".into(),
user_id: Some(USER),
tenant_id: Some(17),
});
context.bind_entry("news", "article");
context
}
#[test]
#[ignore = "requires explicit config/setting.json database.postgres_test"]
fn postgres_capabilities_share_role_transactions_and_reregistration_drops_old_grants() {
fixture(async |case, scope| {
let context = context(case);
let mut executor = case.executor(scope).await;
let key = capability::register_capability(
&mut Tenant::for_test(&mut executor, 17),
scope,
&context,
ID,
)
.await
.unwrap();
assert_eq!(
key,
"capability:news.article.admin.01900000-0000-7000-8000-000000000001"
);
authz::save(
&mut executor,
scope,
&context,
"editor",
"Editor",
false,
std::slice::from_ref(&key),
)
.await
.unwrap();
authz::grant(&mut executor, scope, &context, USER, "editor")
.await
.unwrap();
assert_eq!(
authz::permissions(&mut executor, scope, &context, true)
.await
.unwrap()
.len(),
1
);
let transaction = executor.begin(scope).await.unwrap();
capability::remove_capability(
&mut Tenant::for_test(&mut executor, 17),
scope,
&context,
ID,
)
.await
.unwrap();
assert!(
authz::permissions(&mut executor, scope, &context, true)
.await
.unwrap()
.is_empty()
);
executor.finish(scope, transaction, false).await.unwrap();
assert_eq!(
authz::permissions(&mut executor, scope, &context, true)
.await
.unwrap()
.len(),
1
);
capability::remove_capability(
&mut Tenant::for_test(&mut executor, 17),
scope,
&context,
ID,
)
.await
.unwrap();
capability::register_capability(
&mut Tenant::for_test(&mut executor, 17),
scope,
&context,
ID,
)
.await
.unwrap();
assert!(
authz::permissions(&mut executor, scope, &context, true)
.await
.unwrap()
.is_empty()
);
assert_eq!(
case.count(scope, "SELECT COUNT(*) FROM _dever_role_permission")
.await,
0
);
authz::owner(&mut executor, scope, "front", USER)
.await
.unwrap();
assert!(
capability::has_protected_roles(
&mut Tenant::for_test(&mut executor, 17),
scope,
&context,
USER
)
.await
.unwrap()
);
assert!(
!capability::has_protected_roles(
&mut Tenant::for_test(&mut executor, 17),
scope,
&context,
USER + 1
)
.await
.unwrap()
);
assert_eq!(
authz::permissions(&mut executor, scope, &context, false)
.await
.unwrap()[0]
.kind,
"capability"
);
});
}
#[test]
#[ignore = "requires explicit config/setting.json database.postgres_test"]
fn postgres_capability_removal_serializes_with_grants_in_both_lock_orders() {
fixture(async |case, scope| {
let context = context(case);
let mut first = case.executor(scope).await;
let mut second = case.executor(scope).await;
let blocker = case.pid(scope, &mut first).await;
let waiter = case.pid(scope, &mut second).await;
let key = capability::register_capability(
&mut Tenant::for_test(&mut first, 17),
scope,
&context,
ID,
)
.await
.unwrap();
let transaction = first.begin(scope).await.unwrap();
authz::save(
&mut first,
scope,
&context,
"editor",
"Editor",
false,
std::slice::from_ref(&key),
)
.await
.unwrap();
let remove = async {
capability::remove_capability(
&mut Tenant::for_test(&mut second, 17),
scope,
&context,
ID,
)
.await
};
let commit = async {
case.blocked(scope, waiter, blocker).await;
first.finish(scope, transaction, true).await.unwrap();
};
let (removed, ()) = tokio::join!(remove, commit);
removed.unwrap();
assert_eq!(
case.count(scope, "SELECT COUNT(*) FROM _dever_role_permission")
.await,
0
);
capability::register_capability(&mut Tenant::for_test(&mut first, 17), scope, &context, ID)
.await
.unwrap();
let transaction = first.begin(scope).await.unwrap();
capability::remove_capability(&mut Tenant::for_test(&mut first, 17), scope, &context, ID)
.await
.unwrap();
let grant = authz::save(
&mut second,
scope,
&context,
"late",
"Late",
false,
std::slice::from_ref(&key),
);
let commit = async {
case.blocked(scope, waiter, blocker).await;
first.finish(scope, transaction, true).await.unwrap();
};
let (granted, ()) = tokio::join!(grant, commit);
denied(granted, 400);
assert!(
authz::roles(&mut first, scope, &context, Selector::Id("late".into()))
.await
.unwrap()
.is_empty()
);
assert_eq!(
case.count(scope, "SELECT COUNT(*) FROM _dever_role_permission")
.await,
0
);
});
}
@@ -0,0 +1,140 @@
use super::*;
#[test]
#[ignore = "requires explicit config/setting.json database.postgres_test"]
fn postgres_directory_sync_is_atomic_and_never_broadens_owner_permissions() {
fixture(async |case, scope| {
let mut first = case.executor(scope).await;
let mut second = case.executor(scope).await;
let context = case.context("admin", Some(USER));
authz::synchronize(&mut first, scope, &case.config, &DECLARATIONS)
.await
.unwrap();
authz::owner(&mut first, scope, "admin", USER)
.await
.unwrap();
assert_eq!(
authz::permissions(&mut first, scope, &context, false)
.await
.unwrap()
.len(),
2
);
assert_eq!(
authz::permissions(&mut first, scope, &context, true)
.await
.unwrap()
.len(),
2
);
let owner = first.begin(scope).await.unwrap();
authz::synchronize(&mut first, scope, &case.config, &[])
.await
.unwrap();
denied(
authz::authorize_key(&mut first, scope, &context, CREATE).await,
403,
);
authz::authorize_key(&mut second, scope, &context, CREATE)
.await
.unwrap();
first.finish(scope, owner, false).await.unwrap();
authz::authorize_key(&mut first, scope, &context, CREATE)
.await
.unwrap();
let duplicate = [
Declaration {
topic: "admin",
component: "news",
domain: "article",
action: "create",
method: "POST",
},
Declaration {
topic: "admin",
component: "news",
domain: "article",
action: "create",
method: "DELETE",
},
];
denied(
authz::synchronize(&mut first, scope, &case.config, &duplicate).await,
500,
);
authz::authorize_key(&mut first, scope, &context, REMOVE)
.await
.unwrap();
authz::synchronize(&mut first, scope, &case.config, &DECLARATIONS[1..])
.await
.unwrap();
denied(
authz::authorize_key(&mut first, scope, &context, CREATE).await,
403,
);
authz::authorize_key(&mut first, scope, &context, REMOVE)
.await
.unwrap();
assert_eq!(
authz::permissions(&mut first, scope, &context, true)
.await
.unwrap()
.len(),
1
);
});
}
#[test]
#[ignore = "requires explicit config/setting.json database.postgres_test"]
fn postgres_startup_sync_serializes_new_permissions_before_replacing_the_catalog() {
fixture(async |case, scope| {
let mut first = case.executor(scope).await;
let mut second = case.executor(scope).await;
let context = case.context("admin", Some(USER));
let blocker = case.pid(scope, &mut first).await;
let pid = case.pid(scope, &mut second).await;
let owner = first.begin(scope).await.unwrap();
authz::synchronize(&mut first, scope, &case.config, &DECLARATIONS[..1])
.await
.unwrap();
let replace = authz::synchronize(&mut second, scope, &case.config, &DECLARATIONS[1..]);
let commit = async {
case.blocked(scope, pid, blocker).await;
first.finish(scope, owner, true).await.unwrap();
};
let (result, ()) = tokio::join!(replace, commit);
result.unwrap();
authz::owner(&mut first, scope, "admin", USER)
.await
.unwrap();
let active = authz::permissions(&mut first, scope, &context, true)
.await
.unwrap();
assert_eq!(active.len(), 1);
assert_eq!(active[0].key, REMOVE);
denied(
authz::authorize_key(&mut first, scope, &context, CREATE).await,
403,
);
denied(
authz::save(
&mut first,
scope,
&context,
"stale",
"Stale",
false,
&[CREATE.into()],
)
.await,
400,
);
assert!(
authz::roles(&mut first, scope, &context, Selector::Id("stale".into()))
.await
.unwrap()
.is_empty()
);
});
}
@@ -0,0 +1,149 @@
use super::*;
#[test]
#[ignore = "requires explicit config/setting.json database.postgres_test"]
fn postgres_role_mutations_check_protection_after_acquiring_the_row_lock() {
fixture(async |case, scope| {
let context = case.context("admin", Some(USER));
for operation in 0..5 {
let id = format!("role{operation}");
let mut locker = case.executor(scope).await;
authz::save(&mut locker, scope, &context, &id, "Original", false, &[])
.await
.unwrap();
authz::grant(&mut locker, scope, &context, USER, &id)
.await
.unwrap();
let blocker = case.pid(scope, &mut locker).await;
let owner = locker.begin(scope).await.unwrap();
locker
.query(
scope,
"SELECT id FROM _dever_role WHERE site='admin' AND id=$1 FOR UPDATE",
vec![Parameter::Text(id.clone())],
1,
|row| row.get::<_, String>(0),
)
.await
.unwrap();
let mut worker = case.executor(scope).await;
let pid = case.pid(scope, &mut worker).await;
let mutation = async {
match operation {
0 => authz::save(&mut worker, scope, &context, &id, "Changed", true, &[]).await,
1 => authz::grant(&mut worker, scope, &context, USER + 1, &id).await,
2 => authz::revoke(&mut worker, scope, &context, USER, &id).await,
3 => authz::disable(&mut worker, scope, &context, &id).await,
_ => authz::remove(&mut worker, scope, &context, &id).await,
}
};
let protect = async {
case.blocked(scope, pid, blocker).await;
locker
.execute(
scope,
"UPDATE _dever_role SET protected=true WHERE site='admin' AND id=$1",
vec![Parameter::Text(id.clone())],
)
.await
.unwrap();
locker.finish(scope, owner, true).await.unwrap();
};
let (result, ()) = tokio::join!(mutation, protect);
denied(result, 403);
let role = authz::roles(&mut worker, scope, &context, Selector::Id(id))
.await
.unwrap();
assert_eq!(role[0].name, "Original");
assert!(role[0].protected && role[0].enabled && !role[0].all_permissions);
}
assert_eq!(
case.count(scope, "SELECT COUNT(*) FROM _dever_user_role")
.await,
5
);
});
}
#[test]
#[ignore = "requires explicit config/setting.json database.postgres_test"]
fn postgres_concurrent_save_keeps_one_complete_role_and_owner_creation_cannot_be_overwritten() {
fixture(async |case, scope| {
let mut first = case.executor(scope).await;
let mut second = case.executor(scope).await;
authz::synchronize(&mut first, scope, &case.config, &DECLARATIONS)
.await
.unwrap();
let context = case.context("admin", Some(USER));
for index in 0..8 {
let id = format!("race{index}");
let first_keys = [CREATE.into()];
let second_keys = [REMOVE.into()];
let (left, right) = tokio::join!(
authz::save(
&mut first,
scope,
&context,
&id,
"Create",
false,
&first_keys
),
authz::save(
&mut second,
scope,
&context,
&id,
"Remove",
true,
&second_keys
),
);
left.unwrap();
right.unwrap();
let role = authz::roles(&mut first, scope, &context, Selector::Id(id))
.await
.unwrap();
assert!(
(role[0].name == "Create"
&& !role[0].all_permissions
&& role[0].permission_keys == [CREATE])
|| (role[0].name == "Remove"
&& role[0].all_permissions
&& role[0].permission_keys == [REMOVE])
);
}
let (owner, saved) = tokio::join!(
authz::owner(&mut first, scope, "admin", USER),
authz::save(
&mut second,
scope,
&context,
"owner",
"Ordinary",
false,
&[]
),
);
let role = authz::roles(&mut first, scope, &context, Selector::Id("owner".into()))
.await
.unwrap();
if owner.is_ok() {
denied(saved, 403);
assert!(role[0].protected && role[0].all_permissions && role[0].enabled);
assert_eq!(role[0].name, "Owner");
} else {
denied(owner, 409);
saved.unwrap();
assert!(!role[0].protected && !role[0].all_permissions);
assert_eq!(
case.count(
scope,
"SELECT COUNT(*) FROM _dever_user_role WHERE role_id='owner'"
)
.await,
0
);
}
});
}
@@ -0,0 +1,212 @@
use crate::{
adapter::Document,
api,
api::Request,
auth::{Context, Identity, Service},
authorization::{Declaration, Failure, Selector},
database::{Database, Error, Executor, Options, Parameter},
task::{Runtime, Scope},
};
use futures_util::FutureExt;
use std::{
panic::AssertUnwindSafe,
path::Path,
sync::Arc,
time::{Duration, SystemTime, UNIX_EPOCH},
};
#[path = "access.rs"]
mod authz;
mod capability;
mod catalog;
mod concurrent;
mod roles;
const CREATE: &str = "news.article.admin.create";
const REMOVE: &str = "news.article.admin.remove";
const USER: i64 = 4_294_967_300;
const DECLARATIONS: [Declaration; 3] = [
Declaration {
topic: "admin",
component: "news",
domain: "article",
action: "create",
method: "POST",
},
Declaration {
topic: "admin",
component: "news",
domain: "article",
action: "remove",
method: "DELETE",
},
Declaration {
topic: "front",
component: "news",
domain: "article",
action: "read",
method: "GET",
},
];
struct Case {
database: Database,
schema: String,
config: Arc<api::Config>,
service: Service,
request: Request,
}
impl Case {
async fn executor(&self, scope: &Scope<'_>) -> Executor {
let mut executor = self.database.executor().await.unwrap();
executor
.query(
scope,
"SELECT set_config('search_path',$1,false)",
vec![Parameter::Text(self.schema.clone())],
1,
|row| row.get::<_, String>(0),
)
.await
.unwrap();
executor
}
fn context(&self, site: &str, user: Option<i64>) -> Context<'_> {
let (mut context, _) = self
.service
.prepare(site, &self.request, true, 100)
.unwrap();
if let Some(user) = user {
context.identity_for_authorization_test(Identity {
id: "verified-fixture-user".into(),
user_id: Some(user),
tenant_id: None,
});
}
context
}
async fn count(&self, scope: &Scope<'_>, sql: &'static str) -> i64 {
self.executor(scope)
.await
.query(scope, sql, vec![], 1, |row| row.get::<_, i64>(0))
.await
.unwrap()[0]
}
async fn pid(&self, scope: &Scope<'_>, executor: &mut Executor) -> i64 {
executor
.query(
scope,
"SELECT CAST(pg_backend_pid() AS BIGINT)",
vec![],
1,
|row| row.get::<_, i64>(0),
)
.await
.unwrap()[0]
}
async fn blocked(&self, scope: &Scope<'_>, pid: i64, blocker: i64) {
let mut observer = self.executor(scope).await;
tokio::time::timeout(Duration::from_secs(2), async {
loop {
let waiting = observer.query(scope, "SELECT CAST(CAST($2 AS BIGINT) AS INTEGER) = ANY(pg_blocking_pids(CAST(CAST($1 AS BIGINT) AS INTEGER)))", vec![Parameter::Integer(pid), Parameter::Integer(blocker)], 1, |row| row.get::<_, bool>(0)).await.unwrap()[0];
if waiting { break; }
tokio::task::yield_now().await;
}
}).await.expect("authorization operation did not wait for its row lock");
}
}
fn denied<Value>(result: Result<Value, Failure>, expected: u16) {
assert!(matches!(result, Err(Failure::Access(status)) if status == expected));
}
fn fixture(check: impl AsyncFnOnce(&Case, &Scope<'_>)) {
let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../config/setting.json");
let document = Document::load(&path).expect("cannot read explicit PostgreSQL test settings");
let root: serde_json::Value = document.decode().expect("invalid PostgreSQL test settings");
let value = root
.get("database")
.and_then(|database| database.get("postgres_test"))
.expect("database.postgres_test must explicitly select the test database");
let mut options: Options = serde_json::from_value(value.clone())
.unwrap_or_else(|_| panic!("invalid PostgreSQL test options"));
assert_eq!(options.driver, "postgres");
options.max_connections = 8;
options.wait_timeout_ms = 1000;
options.io_timeout_ms = 5000;
let unique = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
let schema = format!("dever_auth_test_{}_{unique}", std::process::id());
let setting = std::env::temp_dir().join(format!("{schema}.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"]);
std::fs::remove_file(setting).unwrap();
let config = Arc::new(config.unwrap());
let service = Service::new(config.clone(), &[], &[]).unwrap();
Runtime::new(Default::default())
.unwrap()
.run(async |scope| {
let database = Database::open(Path::new("."), options, scope)
.await
.unwrap();
let case = Case {
database,
schema,
config,
service,
request: hyper::Request::builder()
.method("POST")
.uri("/entry")
.body(Default::default())
.unwrap(),
};
let mut owner = case.database.executor().await.unwrap();
owner
.postgres_operation(async |client| {
client
.batch_execute(&format!("CREATE SCHEMA {}", case.schema))
.await
.map_err(Error::from)
})
.await
.unwrap();
let outcome = AssertUnwindSafe(async {
let mut executor = case.executor(scope).await;
for _ in 0..2 {
for binding in authz::schema::BINDINGS {
executor
.schema(scope, binding.table, binding.schema)
.await
.unwrap();
}
}
drop(executor);
tokio::time::timeout(Duration::from_secs(30), check(&case, scope))
.await
.expect("PostgreSQL authorization test deadline exceeded");
})
.catch_unwind()
.await;
let cleanup = owner
.postgres_operation(async |client| {
client
.batch_execute(&format!("DROP SCHEMA {} CASCADE", case.schema))
.await
.map_err(Error::from)
})
.await;
case.database.close();
cleanup.expect("PostgreSQL authorization schema cleanup failed");
if let Err(panic) = outcome {
std::panic::resume_unwind(panic);
}
})
.unwrap();
}
@@ -0,0 +1,347 @@
use super::*;
#[test]
#[ignore = "requires explicit config/setting.json database.postgres_test"]
fn postgres_role_grant_revoke_disable_remove_and_entry_authorization() {
fixture(async |case, scope| {
let mut executor = case.executor(scope).await;
authz::synchronize(&mut executor, scope, &case.config, &DECLARATIONS)
.await
.unwrap();
let mut context = case.context("admin", Some(USER));
let front = case.context("front", Some(USER));
denied(
authz::authorize_entry(
&mut executor,
scope,
&mut context,
"news",
"article",
"create",
)
.await,
403,
);
assert!(context.entry_namespace().is_err());
authz::save(
&mut executor,
scope,
&context,
"editor",
"Editor",
false,
&[CREATE.into(), CREATE.into()],
)
.await
.unwrap();
for _ in 0..2 {
authz::grant(&mut executor, scope, &context, USER, "editor")
.await
.unwrap();
}
authz::authorize_entry(
&mut executor,
scope,
&mut context,
"news",
"article",
"create",
)
.await
.unwrap();
assert_eq!(context.entry_namespace().unwrap(), ("news", "article"));
denied(
authz::authorize_key(&mut executor, scope, &context, REMOVE).await,
403,
);
denied(
authz::authorize_key(&mut executor, scope, &front, CREATE).await,
403,
);
let roles = authz::roles(&mut executor, scope, &context, Selector::User(USER))
.await
.unwrap();
assert_eq!(roles.len(), 1);
assert_eq!(roles[0].permission_keys, [CREATE]);
assert!(!roles[0].all_permissions && roles[0].enabled && !roles[0].protected);
assert_eq!(
authz::permissions(&mut executor, scope, &context, true)
.await
.unwrap()[0]
.key,
CREATE
);
authz::revoke(&mut executor, scope, &context, USER, "editor")
.await
.unwrap();
denied(
authz::authorize_key(&mut executor, scope, &context, CREATE).await,
403,
);
authz::grant(&mut executor, scope, &context, USER, "editor")
.await
.unwrap();
authz::disable(&mut executor, scope, &context, "editor")
.await
.unwrap();
denied(
authz::authorize_key(&mut executor, scope, &context, CREATE).await,
403,
);
authz::save(&mut executor, scope, &context, "editor", "All", true, &[])
.await
.unwrap();
authz::authorize_key(&mut executor, scope, &context, REMOVE)
.await
.unwrap();
authz::remove(&mut executor, scope, &context, "editor")
.await
.unwrap();
assert_eq!(
case.count(scope, "SELECT COUNT(*) FROM _dever_user_role")
.await,
0
);
assert_eq!(
case.count(scope, "SELECT COUNT(*) FROM _dever_role_permission")
.await,
0
);
denied(
authz::grant(&mut executor, scope, &context, USER, "missing").await,
404,
);
denied(
authz::grant(&mut executor, scope, &context, 0, "missing").await,
400,
);
});
}
#[test]
#[ignore = "requires explicit config/setting.json database.postgres_test"]
fn postgres_owner_is_explicit_protected_and_never_authorizes_unknown_keys() {
fixture(async |case, scope| {
let mut executor = case.executor(scope).await;
authz::synchronize(&mut executor, scope, &case.config, &DECLARATIONS)
.await
.unwrap();
let context = case.context("admin", Some(USER));
denied(
authz::authorize_key(&mut executor, scope, &context, CREATE).await,
403,
);
for _ in 0..2 {
authz::owner(&mut executor, scope, "admin", USER)
.await
.unwrap();
}
authz::authorize_key(&mut executor, scope, &context, CREATE)
.await
.unwrap();
denied(
authz::authorize_key(&mut executor, scope, &context, "news.article.admin.unknown")
.await,
403,
);
denied(
authz::save(
&mut executor,
scope,
&context,
"owner",
"Changed",
false,
&[],
)
.await,
403,
);
denied(
authz::disable(&mut executor, scope, &context, "owner").await,
403,
);
denied(
authz::remove(&mut executor, scope, &context, "owner").await,
403,
);
denied(
authz::grant(&mut executor, scope, &context, 8, "owner").await,
403,
);
denied(
authz::revoke(&mut executor, scope, &context, USER, "owner").await,
403,
);
let owner = authz::roles(&mut executor, scope, &context, Selector::Id("owner".into()))
.await
.unwrap();
assert_eq!(owner.len(), 1);
assert!(owner[0].protected && owner[0].all_permissions && owner[0].enabled);
let front = case.context("front", Some(USER));
authz::save(
&mut executor,
scope,
&front,
"owner",
"Ordinary",
false,
&[],
)
.await
.unwrap();
denied(authz::owner(&mut executor, scope, "front", USER).await, 409);
assert!(
authz::roles(&mut executor, scope, &front, Selector::User(USER))
.await
.unwrap()
.is_empty()
);
});
}
#[test]
#[ignore = "requires explicit config/setting.json database.postgres_test"]
fn postgres_role_writes_reuse_ambient_transaction_and_roll_back_atomically() {
fixture(async |case, scope| {
let mut executor = case.executor(scope).await;
authz::synchronize(&mut executor, scope, &case.config, &DECLARATIONS)
.await
.unwrap();
let context = case.context("admin", Some(USER));
let owner = executor.begin(scope).await.unwrap();
authz::save(
&mut executor,
scope,
&context,
"editor",
"Editor",
false,
&[CREATE.into()],
)
.await
.unwrap();
authz::grant(&mut executor, scope, &context, USER, "editor")
.await
.unwrap();
authz::authorize_key(&mut executor, scope, &context, CREATE)
.await
.unwrap();
assert_eq!(
case.count(scope, "SELECT COUNT(*) FROM _dever_role").await,
0
);
denied(
authz::save(
&mut executor,
scope,
&context,
"editor",
"Invalid",
true,
&["unknown".into()],
)
.await,
400,
);
assert!(executor.finish(scope, owner, true).await.is_err());
assert_eq!(
case.count(scope, "SELECT COUNT(*) FROM _dever_role").await,
0
);
assert_eq!(
case.count(scope, "SELECT COUNT(*) FROM _dever_user_role")
.await,
0
);
authz::save(
&mut executor,
scope,
&context,
"editor",
"Editor",
false,
&[CREATE.into()],
)
.await
.unwrap();
denied(
authz::save(
&mut executor,
scope,
&context,
"editor",
"Invalid",
true,
&["news.article.front.read".into()],
)
.await,
400,
);
let role = authz::roles(&mut executor, scope, &context, Selector::All)
.await
.unwrap();
assert_eq!(role[0].name, "Editor");
assert_eq!(role[0].permission_keys, [CREATE]);
assert!(!role[0].all_permissions);
});
}
#[test]
#[ignore = "requires explicit config/setting.json database.postgres_test"]
fn postgres_missing_identity_is_rejected_before_any_database_query() {
fixture(async |case, scope| {
let mut executor = case.executor(scope).await;
executor
.postgres_operation(async |client| {
client
.batch_execute("SET search_path TO pg_catalog")
.await
.map_err(Error::from)
})
.await
.unwrap();
let mut anonymous = case.context("admin", None);
denied(
authz::roles(&mut executor, scope, &anonymous, Selector::All).await,
401,
);
denied(
authz::permissions(&mut executor, scope, &anonymous, false).await,
401,
);
denied(
authz::save(&mut executor, scope, &anonymous, "x", "X", true, &[]).await,
401,
);
denied(
authz::grant(&mut executor, scope, &anonymous, USER, "x").await,
401,
);
denied(
authz::revoke(&mut executor, scope, &anonymous, USER, "x").await,
401,
);
denied(
authz::disable(&mut executor, scope, &anonymous, "x").await,
401,
);
denied(
authz::remove(&mut executor, scope, &anonymous, "x").await,
401,
);
denied(
authz::authorize_entry(
&mut executor,
scope,
&mut anonymous,
"news",
"article",
"create",
)
.await,
401,
);
assert!(anonymous.entry_namespace().is_err());
});
}
@@ -0,0 +1,127 @@
use super::access_tests::{self, Fixture};
use crate::{
adapter::Document,
database::{Database, Options, Parameter},
task::{Runtime, Scope},
};
use futures_util::FutureExt;
use std::{panic::AssertUnwindSafe, path::Path};
fn fixture(check: impl AsyncFnOnce(&Fixture, &Scope<'_>)) {
let fixture = Fixture::new();
Runtime::new(Default::default())
.unwrap()
.run(async |scope| {
let document = Document::load(
&Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../config/setting.json"),
)
.unwrap();
let settings: serde_json::Value = document.decode().unwrap();
let admin: Options =
serde_json::from_value(settings["database"]["postgres_test"].clone()).unwrap();
let mut options = settings["database"]["postgres_tenant_test"].clone();
let template = options["url"].as_str().unwrap();
assert_eq!(template.matches("{case}").count(), 1);
assert!(
template
.parse::<tokio_postgres::Config>()
.unwrap()
.get_dbname()
.unwrap()
.contains("{case}")
);
let token = crate::database::scalar::generated_uuid()
.unwrap()
.to_string()
.replace('-', "");
let url = template.replace("{case}", &token);
let control = url
.parse::<tokio_postgres::Config>()
.unwrap()
.get_dbname()
.unwrap()
.to_owned();
assert!(
control
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_')
);
let prefix = format!("auth_{token}");
let names = [
format!("{prefix}_17"),
format!("{prefix}_18"),
control.clone(),
];
let admin = Database::open(Path::new("."), admin, scope).await.unwrap();
let mut executor = admin.executor().await.unwrap();
for name in &names {
assert_eq!(
executor
.query(
scope,
"SELECT COUNT(*) FROM pg_database WHERE datname=$1",
vec![Parameter::Text(name.clone())],
1,
|row| row.get::<_, i64>(0)
)
.await
.unwrap(),
[0]
);
}
executor
.postgres_operation(async |client| {
client
.batch_execute(&format!("CREATE DATABASE \"{control}\""))
.await
.map_err(Into::into)
})
.await
.unwrap();
options["url"] = url.into();
options["tenant_database_prefix"] = prefix.into();
options["max_connections"] = 1.into();
let result = AssertUnwindSafe(async {
let mut config: serde_json::Value =
serde_json::from_slice(&std::fs::read(&fixture.setting).unwrap()).unwrap();
config["database"]["default"] = options;
std::fs::write(&fixture.setting, config.to_string()).unwrap();
tokio::time::timeout(std::time::Duration::from_secs(30), check(&fixture, scope))
.await
.unwrap();
})
.catch_unwind()
.await;
for name in &names {
executor
.postgres_operation(async |client| {
client
.batch_execute(&format!(
"DROP DATABASE IF EXISTS \"{name}\" WITH (FORCE)"
))
.await
.map_err(Into::into)
})
.await
.expect("owned tenant authorization database cleanup failed");
}
drop(executor);
admin.close();
if let Err(panic) = result {
std::panic::resume_unwind(panic);
}
})
.unwrap();
}
#[test]
#[ignore = "requires explicit isolated postgres_tenant_test template and CREATEDB"]
fn postgres_two_physical_tenants_keep_roles_and_capabilities_out_of_the_control_catalog() {
fixture(async |fixture, scope| access_tests::routing(fixture, scope).await);
}
#[test]
#[ignore = "requires explicit isolated postgres_tenant_test template and CREATEDB"]
fn postgres_tenant_role_capability_and_model_rollback_uses_one_physical_transaction() {
fixture(async |fixture, scope| access_tests::rollback(fixture, scope).await);
}