Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user