pub(crate) use crate::object_permissions::has_hook;
pub use crate::object_permissions::{is_allowed, ObjectPermissionFn};
#[deprecated(
since = "0.62.0",
note = "use `rustango::object_permissions::ObjectPermission`"
)]
pub type AdminObjectPermission = crate::object_permissions::ObjectPermission;
#[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) fn default_locked_flags(
model: &'static crate::core::ModelSchema,
locked: &[&'static str],
values: &mut Vec<(&'static str, crate::core::SqlValue)>,
) {
for f in model.scalar_fields() {
if f.ty == crate::core::FieldType::Bool
&& !f.nullable
&& f.default.is_none()
&& locked.contains(&f.name)
&& !values.iter().any(|(c, _)| *c == f.column)
{
values.push((f.column, crate::core::SqlValue::Bool(false)));
}
}
}
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::object_permissions::register_framework_object_permission!(
$table,
"change",
crate::object_permissions::superuser_row_needs_superuser
);
crate::object_permissions::register_framework_object_permission!(
$table,
"delete",
crate::object_permissions::superuser_row_needs_superuser
);
};
}
pub(crate) use lock_user_credentials;
#[cfg(all(test, feature = "tenancy", any(feature = "mcp", feature = "admin-sso")))]
mod tests {
use super::*;
#[cfg(all(feature = "tenancy", feature = "mcp"))]
#[test]
fn credential_owner_is_superuser_only() {
for table in ["rustango_api_keys", "rustango_agents"] {
for action in [AdminWrite::Add, AdminWrite::Change] {
assert_eq!(
superuser_fields(table, action),
["user_id"],
"{table} {action:?}"
);
}
}
}
#[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}");
}
}
}
}