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