336 lines
9.6 KiB
Rust
336 lines
9.6 KiB
Rust
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;
|
|
}
|