use axum::http::request::Parts;
use serde_json::Value;
pub type ObjectPermissionFn = fn(&Parts, Option<&Value>) -> bool;
pub struct AdminObjectPermission {
pub table: &'static str,
pub action: &'static str,
pub check: ObjectPermissionFn,
}
inventory::collect!(AdminObjectPermission);
#[must_use]
pub fn is_allowed(table: &str, action: &str, parts: &Parts, row: Option<&Value>) -> bool {
for entry in inventory::iter::<AdminObjectPermission> {
if entry.table != table || entry.action != action {
continue;
}
if !(entry.check)(parts, row) {
return false;
}
}
true
}
#[must_use]
pub(crate) fn has_hook(table: &str, action: &str) -> bool {
inventory::iter::<AdminObjectPermission>
.into_iter()
.any(|e| e.table == table && e.action == action)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum AdminWrite {
Add,
Change,
}
pub struct AdminSuperuserFields {
pub table: &'static str,
pub action: AdminWrite,
pub fields: &'static [&'static str],
}
inventory::collect!(AdminSuperuserFields);
pub(crate) fn superuser_fields(table: &str, action: AdminWrite) -> Vec<&'static str> {
inventory::iter::<AdminSuperuserFields>
.into_iter()
.filter(|e| e.table == table && e.action == action)
.flat_map(|e| e.fields.iter().copied())
.collect()
}
pub(crate) struct LockedFields {
pub(crate) readonly: &'static [&'static str],
pub(crate) superuser_only: Vec<&'static str>,
}
impl LockedFields {
pub(crate) fn contains(&self, name: &str) -> bool {
self.readonly.contains(&name) || self.superuser_only.contains(&name)
}
pub(crate) fn all(&self) -> impl Iterator<Item = &'static str> + '_ {
self.readonly
.iter()
.copied()
.chain(self.superuser_only.iter().copied())
}
}
#[macro_export]
macro_rules! register_admin_superuser_fields {
($model:ty, $action:ident, [$($field:ident),+ $(,)?]) => {
const _: () = {
#[allow(dead_code)]
fn fields_exist(m: &$model) {
$(let _ = &m.$field;)+
}
};
$crate::inventory::submit! {
$crate::admin::object_permissions::AdminSuperuserFields {
table: <$model as $crate::core::Model>::SCHEMA.table,
action: $crate::admin::object_permissions::AdminWrite::$action,
fields: &[$(stringify!($field)),+],
}
}
};
}
macro_rules! lock_user_credentials {
($model:ty, $table:literal) => {
crate::register_admin_superuser_fields!($model, Add, [is_superuser]);
crate::register_admin_superuser_fields!($model, Change, [is_superuser, password_hash]);
crate::register_admin_object_permission!(
$table,
"change",
crate::admin::session::superuser_row_needs_superuser
);
crate::register_admin_object_permission!(
$table,
"delete",
crate::admin::session::superuser_row_needs_superuser
);
};
}
pub(crate) use lock_user_credentials;
#[macro_export]
macro_rules! register_admin_object_permission {
($table:expr, $action:expr, $check:expr $(,)?) => {
$crate::inventory::submit! {
$crate::admin::object_permissions::AdminObjectPermission {
table: $table,
action: $action,
check: {
const _CHECK: $crate::admin::object_permissions::ObjectPermissionFn = $check;
_CHECK
},
}
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::Request;
fn parts() -> Parts {
let req: Request<()> = Request::builder().uri("/").body(()).unwrap();
let (p, ()) = req.into_parts();
p
}
#[test]
fn is_allowed_returns_true_when_no_hooks_registered() {
let p = parts();
assert!(is_allowed("nonexistent", "change", &p, None));
}
#[cfg(all(feature = "tenancy", feature = "mcp"))]
#[test]
fn credential_owner_is_superuser_only() {
for table in ["rustango_api_keys", "rustango_agents"] {
assert_eq!(
superuser_fields(table, AdminWrite::Change),
["user_id"],
"{table}"
);
assert!(
superuser_fields(table, AdminWrite::Add).is_empty(),
"{table}"
);
}
}
#[cfg(all(feature = "tenancy", feature = "admin-sso"))]
#[test]
fn user_credentials_are_superuser_only() {
for table in ["rustango_users", "rustango_admin_users"] {
let mut add = superuser_fields(table, AdminWrite::Add);
add.sort_unstable();
assert_eq!(add, ["email", "is_superuser"], "{table}");
let mut change = superuser_fields(table, AdminWrite::Change);
change.sort_unstable();
assert_eq!(
change,
["email", "is_superuser", "password_hash"],
"{table}"
);
for action in ["change", "delete"] {
assert!(has_hook(table, action), "{table} {action}");
}
}
}
#[test]
fn fn_pointer_coercion_smoke_test() {
fn _h(_p: &Parts, _row: Option<&Value>) -> bool {
true
}
let _f: ObjectPermissionFn = _h;
}
}