mod actor;
mod scope;
#[cfg(feature = "tenancy")]
pub(crate) use actor::RequestTenant;
pub use actor::{is_superuser, Actor};
pub use scope::{for_table as queryset_scopes_for, QuerySetScope, QuerySetScopeFn};
pub(crate) use scope::{row_pk, view_hook_rows, warn_scan_cap, ViewHookRows};
use axum::http::request::Parts;
use serde_json::Value;
pub type ObjectPermissionFn = fn(&Parts, Option<&Value>) -> bool;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum HookOrigin {
App,
#[doc(hidden)]
Framework,
}
pub struct ObjectPermission {
pub table: &'static str,
pub action: &'static str,
pub check: ObjectPermissionFn,
pub origin: HookOrigin,
}
inventory::collect!(ObjectPermission);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Hooks {
All,
FrameworkOnly,
}
impl Hooks {
fn runs(self, entry: &ObjectPermission) -> bool {
self == Self::All || entry.origin == HookOrigin::Framework
}
pub(crate) fn allows(
self,
table: &str,
action: &str,
parts: &Parts,
row: Option<&Value>,
) -> bool {
inventory::iter::<ObjectPermission>
.into_iter()
.filter(|e| e.table == table && e.action == action && self.runs(e))
.all(|e| (e.check)(parts, row))
}
pub(crate) fn any(self, table: &str, action: &str) -> bool {
inventory::iter::<ObjectPermission>
.into_iter()
.any(|e| e.table == table && e.action == action && self.runs(e))
}
pub(crate) fn scope_filters_empty(self, table: &str) -> bool {
self == Self::FrameworkOnly || scope::for_table(table).is_empty()
}
pub(crate) fn scope_filters(self, table: &str, parts: &Parts) -> Vec<crate::core::Filter> {
match self {
Self::All => scope::for_table(table)
.iter()
.flat_map(|h| (h.hook)(parts))
.collect(),
Self::FrameworkOnly => Vec::new(),
}
}
}
#[must_use]
pub fn is_allowed(table: &str, action: &str, parts: &Parts, row: Option<&Value>) -> bool {
Hooks::All.allows(table, action, parts, row)
}
#[must_use]
pub(crate) fn has_hook(table: &str, action: &str) -> bool {
Hooks::All.any(table, action)
}
#[macro_export]
macro_rules! register_object_permission {
($table:expr, $action:expr, $check:expr $(,)?) => {
$crate::inventory::submit! {
$crate::object_permissions::ObjectPermission {
table: $table,
action: $action,
check: {
const _CHECK: $crate::object_permissions::ObjectPermissionFn = $check;
_CHECK
},
origin: $crate::object_permissions::HookOrigin::App,
}
}
};
}
#[deprecated(since = "0.62.0", note = "use `register_object_permission!`")]
#[macro_export]
macro_rules! register_admin_object_permission {
($table:expr, $action:expr, $check:expr $(,)?) => {
$crate::register_object_permission!($table, $action, $check);
};
}
macro_rules! register_framework_object_permission {
($table:expr, $action:expr, $check:expr $(,)?) => {
$crate::inventory::submit! {
$crate::object_permissions::ObjectPermission {
table: $table,
action: $action,
check: {
const _CHECK: $crate::object_permissions::ObjectPermissionFn = $check;
_CHECK
},
origin: $crate::object_permissions::HookOrigin::Framework,
}
}
};
}
pub(crate) use register_framework_object_permission;
#[cfg(feature = "sso")]
pub(crate) fn superuser_only(parts: &Parts, _: Option<&Value>) -> bool {
is_superuser(parts)
}
pub(crate) fn superuser_row_needs_superuser(parts: &Parts, row: Option<&Value>) -> bool {
let row_is_superuser = row.and_then(|r| r.get("is_superuser")).is_some_and(|v| {
v.as_bool()
.unwrap_or_else(|| v.as_i64().is_some_and(|n| n != 0))
});
!row_is_superuser || is_superuser(parts)
}
#[cfg(test)]
mod tests {
use super::*;
fn parts() -> Parts {
axum::http::Request::builder()
.uri("/")
.body(())
.unwrap()
.into_parts()
.0
}
fn deny(_: &Parts, _: Option<&Value>) -> bool {
false
}
crate::register_object_permission!("op_unit_app", "view", deny);
register_framework_object_permission!("op_unit_fw", "view", deny);
#[test]
fn no_hook_allows() {
assert!(is_allowed("nonexistent", "change", &parts(), None));
}
#[test]
fn framework_only_skips_app_hooks() {
let p = parts();
assert!(!Hooks::All.allows("op_unit_app", "view", &p, None));
assert!(Hooks::FrameworkOnly.allows("op_unit_app", "view", &p, None));
assert!(!Hooks::FrameworkOnly.any("op_unit_app", "view"));
assert!(!Hooks::FrameworkOnly.allows("op_unit_fw", "view", &p, None));
assert!(Hooks::FrameworkOnly.any("op_unit_fw", "view"));
}
#[test]
fn a_superuser_row_needs_a_superuser() {
let su = serde_json::json!({"is_superuser": true});
let su_int = serde_json::json!({"is_superuser": 1});
let member = serde_json::json!({"is_superuser": false});
let anon = parts();
assert!(!superuser_row_needs_superuser(&anon, Some(&su)));
assert!(!superuser_row_needs_superuser(&anon, Some(&su_int)));
assert!(superuser_row_needs_superuser(&anon, Some(&member)));
assert!(superuser_row_needs_superuser(&anon, None));
}
}