#![allow(dead_code)]
use axum::body::Body;
use axum::http::{Request, StatusCode, header};
use http_body_util::BodyExt;
use serde::{Deserialize, Serialize};
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use tokio::sync::{Mutex, OnceCell};
use tower::ServiceExt;
use umbral_admin::AdminPlugin;
use umbral_auth::{AuthPlugin, AuthUser, create_user};
use umbral_permissions::PermissionsPlugin;
use umbral_sessions::SessionsPlugin;
#[derive(Debug, sqlx::FromRow, Serialize, Deserialize, umbral::orm::Model)]
#[umbral(table = "wsecret")]
pub struct WSecret {
pub id: i64,
pub data: String,
}
#[derive(Debug, sqlx::FromRow, Serialize, Deserialize, umbral::orm::Model)]
#[umbral(table = "wpublic")]
pub struct WPublic {
pub id: i64,
pub name: String,
}
const SECRET_MARKER: &str = "zzsecretmarkerzz";
const PUBLIC_MARKER: &str = "zzpublicmarkerzz";
static BOOT: OnceCell<axum::Router> = OnceCell::const_new();
static LOCK: Mutex<()> = Mutex::const_new(());
async fn boot() -> &'static axum::Router {
BOOT.get_or_init(|| async {
let settings = umbral::Settings::from_env().expect("settings");
let tmp = tempfile::tempdir().expect("tempdir");
let path = tmp.path().join("authz_web7.sqlite");
std::mem::forget(tmp);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect_with(
SqliteConnectOptions::new().busy_timeout(std::time::Duration::from_secs(5))
.filename(&path)
.create_if_missing(true),
)
.await
.expect("pool");
let app = umbral::App::builder()
.settings(settings)
.database("default", pool)
.plugin(AuthPlugin::<AuthUser>::default())
.plugin(SessionsPlugin::default().without_auto_layer())
.plugin(PermissionsPlugin)
.plugin(AdminPlugin::default())
.model::<WSecret>()
.model::<WPublic>()
.build()
.expect("App::build");
let migration_dir = tempfile::tempdir().expect("migration dir");
let migration_dir_path = migration_dir.path().to_path_buf();
std::mem::forget(migration_dir);
umbral::migrate::make_in(&migration_dir_path)
.await
.expect("make migrations");
umbral::migrate::run_in(&migration_dir_path)
.await
.expect("run migrations");
umbral_permissions::seed_standard_permissions_for_tests()
.await
.expect("seed permissions");
let pool = umbral::db::pool();
sqlx::query("INSERT INTO wsecret (data) VALUES (?)")
.bind(SECRET_MARKER)
.execute(&pool)
.await
.expect("insert wsecret row");
sqlx::query("INSERT INTO wpublic (name) VALUES (?)")
.bind(PUBLIC_MARKER)
.execute(&pool)
.await
.expect("insert wpublic row");
for (model, codename) in [("wsecret", "app.view_wsecret"), ("wpublic", "app.view_wpublic")]
{
sqlx::query(
"INSERT OR IGNORE INTO permissions_contenttype (app_label, model) VALUES ('app', ?)",
)
.bind(model)
.execute(&pool)
.await
.expect("insert content type");
let ct_id: i64 = sqlx::query_scalar(
"SELECT id FROM permissions_contenttype WHERE app_label = 'app' AND model = ?",
)
.bind(model)
.fetch_one(&pool)
.await
.expect("fetch content type id");
sqlx::query(
"INSERT OR IGNORE INTO permissions_permission (codename, content_type_id, name) \
VALUES (?, ?, 'view')",
)
.bind(codename)
.bind(ct_id)
.execute(&pool)
.await
.expect("insert permission");
}
let restricted = create_user("web7_restricted", "r@example.com", "pass123")
.await
.expect("create restricted");
sqlx::query("UPDATE auth_user SET is_staff = 1 WHERE id = ?")
.bind(restricted.id)
.execute(&pool)
.await
.expect("set staff");
sqlx::query(
"INSERT OR IGNORE INTO permissions_userpermission (user_id, permission_id) \
VALUES (?, 'app.view_wpublic')",
)
.bind(restricted.id.to_string())
.execute(&pool)
.await
.expect("grant view_wpublic");
let privileged = create_user("web7_privileged", "p@example.com", "pass123")
.await
.expect("create privileged");
sqlx::query("UPDATE auth_user SET is_staff = 1 WHERE id = ?")
.bind(privileged.id)
.execute(&pool)
.await
.expect("set staff");
for perm in ["app.view_wpublic", "app.view_wsecret"] {
sqlx::query(
"INSERT OR IGNORE INTO permissions_userpermission (user_id, permission_id) \
VALUES (?, ?)",
)
.bind(privileged.id.to_string())
.bind(perm)
.execute(&pool)
.await
.expect("grant privileged perm");
}
let superuser = create_user("web7_super", "s@example.com", "pass123")
.await
.expect("create super");
sqlx::query("UPDATE auth_user SET is_staff = 1, is_superuser = 1 WHERE id = ?")
.bind(superuser.id)
.execute(&pool)
.await
.expect("set superuser");
app.into_router()
})
.await
}
async fn cookie_for(username: &str) -> String {
boot().await;
let pool = umbral::db::pool();
let user: AuthUser = sqlx::query_as(
"SELECT id, username, email, password_hash, is_active, is_staff, is_superuser, \
date_joined, last_login, email_verified_at FROM auth_user WHERE username = ?",
)
.bind(username)
.fetch_one(&pool)
.await
.expect("lookup user");
let tok = umbral_sessions::create_session(Some(user.id.to_string()), None)
.await
.expect("session");
format!("umbral_session={tok}")
}
async fn get(uri: &str, cookie: &str) -> (StatusCode, String) {
let router = boot().await.clone();
let resp = router
.oneshot(
Request::builder()
.uri(uri)
.header(header::COOKIE, cookie)
.body(Body::empty())
.unwrap(),
)
.await
.expect("oneshot");
let status = resp.status();
let body = resp.into_body().collect().await.unwrap().to_bytes();
(status, String::from_utf8_lossy(&body).into_owned())
}
#[tokio::test]
async fn palette_search_excludes_unviewable_model_for_restricted() {
let _g = LOCK.lock().await;
let cookie = cookie_for("web7_restricted").await;
let (status, body) = get("/admin/api/palette/search?q=zz", &cookie).await;
assert_eq!(status, StatusCode::OK, "palette search should 200");
assert!(
!body.contains(SECRET_MARKER),
"restricted user must NOT see wsecret rows in palette search; body: {body}"
);
assert!(
body.contains(PUBLIC_MARKER),
"restricted user SHOULD see wpublic rows they can view; body: {body}"
);
}
#[tokio::test]
async fn palette_search_includes_both_models_for_privileged() {
let _g = LOCK.lock().await;
let cookie = cookie_for("web7_privileged").await;
let (status, body) = get("/admin/api/palette/search?q=zz", &cookie).await;
assert_eq!(status, StatusCode::OK);
assert!(
body.contains(SECRET_MARKER) && body.contains(PUBLIC_MARKER),
"privileged user should see both markers; body: {body}"
);
}
#[tokio::test]
async fn palette_search_includes_both_models_for_superuser() {
let _g = LOCK.lock().await;
let cookie = cookie_for("web7_super").await;
let (status, body) = get("/admin/api/palette/search?q=zz", &cookie).await;
assert_eq!(status, StatusCode::OK);
assert!(
body.contains(SECRET_MARKER) && body.contains(PUBLIC_MARKER),
"superuser should see both markers; body: {body}"
);
}
#[tokio::test]
async fn filter_dialog_gated_by_view_permission() {
let _g = LOCK.lock().await;
let cookie = cookie_for("web7_restricted").await;
let (secret_status, _) = get("/admin/wsecret/filter-dialog", &cookie).await;
assert_eq!(
secret_status,
StatusCode::FORBIDDEN,
"restricted user must be 403 on the unviewable model's filter dialog"
);
let (public_status, _) = get("/admin/wpublic/filter-dialog", &cookie).await;
assert_eq!(
public_status,
StatusCode::OK,
"restricted user CAN open the filter dialog for a model they can view"
);
}
#[tokio::test]
async fn filter_dialog_allowed_for_privileged() {
let _g = LOCK.lock().await;
let cookie = cookie_for("web7_privileged").await;
let (status, _) = get("/admin/wsecret/filter-dialog", &cookie).await;
assert_eq!(status, StatusCode::OK);
}
#[tokio::test]
async fn history_gated_by_view_permission() {
let _g = LOCK.lock().await;
let cookie = cookie_for("web7_restricted").await;
let (secret_status, _) = get("/admin/wsecret/1/history", &cookie).await;
assert_eq!(
secret_status,
StatusCode::FORBIDDEN,
"restricted user must be 403 on the unviewable model's history"
);
let (public_status, _) = get("/admin/wpublic/1/history", &cookie).await;
assert_ne!(
public_status,
StatusCode::FORBIDDEN,
"restricted user must clear the permission gate for a viewable model"
);
}