💥 (admin): Exige un permiso propio en cada página

This commit is contained in:
Manuel Cillero 2026-10-04 23:50:58 +02:00
parent 2299e63585
commit f337119219
6 changed files with 31 additions and 79 deletions

View file

@ -87,9 +87,8 @@ impl ActionBag {
/// bag.add(AdminSection { /// bag.add(AdminSection {
/// key: "tools".to_owned(), /// key: "tools".to_owned(),
/// path: "/admin/tools".to_owned(), /// path: "/admin/tools".to_owned(),
/// title: Lc::n("Tools"), /// title: Lc::n("Tools"),
/// permission: None, /// weight: 60,
/// weight: 60,
/// }); /// });
/// } /// }
/// // En Extension::actions(): /// // En Extension::actions():
@ -130,10 +129,18 @@ impl DeclareAdminSections {
/// # Ejemplo /// # Ejemplo
/// ///
/// ```rust,no_run /// ```rust,no_run
/// use pagetop::locale::Lc; /// use pagetop::prelude::*;
/// use pagetop_admin::action::{DeclareAdminPages, PageBag}; /// use pagetop_admin::action::{DeclareAdminPages, PageBag};
/// use pagetop_admin::registry::{AdminPage, AdminPageKind}; /// use pagetop_admin::registry::{AdminPage, AdminPageKind};
/// ///
/// struct ExportPermission;
///
/// impl Permission for ExportPermission {
/// fn key(&self) -> CowStr {
/// "tools:export".into()
/// }
/// }
///
/// fn declare_pages(bag: &mut PageBag) { /// fn declare_pages(bag: &mut PageBag) {
/// bag.add(AdminPage { /// bag.add(AdminPage {
/// path: "/admin/tools/export".to_owned(), /// path: "/admin/tools/export".to_owned(),
@ -141,7 +148,7 @@ impl DeclareAdminSections {
/// title: Lc::n("Export"), /// title: Lc::n("Export"),
/// description: Some(Lc::n("Export site data.")), /// description: Some(Lc::n("Export site data.")),
/// weight: 0, /// weight: 0,
/// permission: None, /// permission: &ExportPermission,
/// kind: AdminPageKind::View, /// kind: AdminPageKind::View,
/// }); /// });
/// } /// }

View file

@ -69,7 +69,6 @@ fn render_sections(cx: &Context, candidates: &[&'static AdminSection]) -> Option
let sections_with_pages: Vec<_> = candidates let sections_with_pages: Vec<_> = candidates
.iter() .iter()
.copied() .copied()
.filter(|section| section.is_visible(cx))
.map(|section| { .map(|section| {
let pages: Vec<_> = reg let pages: Vec<_> = reg
.pages_for_section(&section.key) .pages_for_section(&section.key)
@ -131,7 +130,7 @@ pub async fn config_form_get(request: HttpRequest) -> Result<Markup, ErrorPage>
return Err(ErrorPage::NotFound(Some(request.clone()))); return Err(ErrorPage::NotFound(Some(request.clone())));
}; };
require_permission(&request, page.permission_key())?; require_permission(&request, page.permission)?;
let title = page.title.clone(); let title = page.title.clone();
let mut form = ConfigForm::with_schema(schema.clone()); let mut form = ConfigForm::with_schema(schema.clone());
@ -170,7 +169,7 @@ pub async fn config_form_post(
return Err(ErrorPage::NotFound(Some(request.clone()))); return Err(ErrorPage::NotFound(Some(request.clone())));
}; };
require_permission(&request, page.permission_key())?; require_permission(&request, page.permission)?;
let mut save_error = false; let mut save_error = false;

View file

@ -50,7 +50,7 @@ fn declare_pages(bag: &mut PageBag) {
title: Lc::n("My App"), title: Lc::n("My App"),
description: Some(Lc::n("Configure My App.")), description: Some(Lc::n("Configure My App.")),
weight: 0, weight: 0,
permission: Some(&MyPermission::Config), permission: &MyPermission::Config,
kind: AdminPageKind::View, kind: AdminPageKind::View,
}); });
} }
@ -63,7 +63,7 @@ enum MyPermission {
impl Permission for MyPermission { impl Permission for MyPermission {
fn key(&self) -> CowStr { fn key(&self) -> CowStr {
match self { match self {
Self::Config => "myapp.config".into(), Self::Config => "myapp:config".into(),
} }
} }
} }

View file

@ -14,38 +14,12 @@ use crate::action::{
}; };
use crate::settings::SettingsSchema; use crate::settings::SettingsSchema;
// **< AdminPermission >****************************************************************************
/// Permisos propios de `pagetop-admin`.
#[derive(Clone, Copy, Debug)]
pub enum AdminPermission {
/// Acceso por defecto a una página de administración que no declara un permiso propio.
Access,
/// Acceso a la sección integrada "people".
AccessPeople,
/// Acceso a la sección integrada "structure".
AccessStructure,
/// Acceso a la sección integrada "config".
AccessConfig,
/// Acceso a la sección integrada "reports".
AccessReports,
}
impl Permission for AdminPermission {
fn key(&self) -> CowStr {
match self {
Self::Access => "admin:access".into(),
Self::AccessPeople => "admin.access_people".into(),
Self::AccessStructure => "admin.access_structure".into(),
Self::AccessConfig => "admin.access_config".into(),
Self::AccessReports => "admin.access_reports".into(),
}
}
}
// **< Tipos del registro >************************************************************************* // **< Tipos del registro >*************************************************************************
/// Sección del panel de administración (agrupación en el sidebar). /// Sección del panel de administración (agrupación de páginas en el menú y en el *dashboard*).
///
/// Una sección no tiene permiso propio: se muestra a quien pueda acceder a alguna de sus páginas, y
/// se oculta si no le queda ninguna.
#[derive(Clone)] #[derive(Clone)]
pub struct AdminSection { pub struct AdminSection {
/// Identificador único de la sección (p. ej. `"config"`). /// Identificador único de la sección (p. ej. `"config"`).
@ -54,24 +28,10 @@ pub struct AdminSection {
pub path: String, pub path: String,
/// Título visible en el sidebar. /// Título visible en el sidebar.
pub title: Lc, pub title: Lc,
/// Permiso requerido para ver la sección (`None` = siempre visible).
pub permission: Option<PermissionRef>,
/// Peso para ordenar en el sidebar (menor = antes). /// Peso para ordenar en el sidebar (menor = antes).
pub weight: i32, pub weight: i32,
} }
impl AdminSection {
/// Devuelve `true` si el usuario actual puede ver esta sección.
pub fn is_visible(&self, cx: &Context) -> bool {
match self.permission {
None => true,
Some(permission) => cx
.request()
.is_some_and(|request| has_permission(request, permission)),
}
}
}
/// Página del panel de administración. /// Página del panel de administración.
#[derive(Clone)] #[derive(Clone)]
pub struct AdminPage { pub struct AdminPage {
@ -85,22 +45,18 @@ pub struct AdminPage {
pub description: Option<Lc>, pub description: Option<Lc>,
/// Peso dentro de la sección (menor = antes). /// Peso dentro de la sección (menor = antes).
pub weight: i32, pub weight: i32,
/// Permiso requerido para acceder (`None` = requiere [`AdminPermission::Access`]). /// Permiso requerido para acceder. Es obligatorio: cada página declara el suyo, y es también
pub permission: Option<PermissionRef>, /// el que decide si su sección se muestra.
pub permission: PermissionRef,
/// Tipo de página y datos asociados. /// Tipo de página y datos asociados.
pub kind: AdminPageKind, pub kind: AdminPageKind,
} }
impl AdminPage { impl AdminPage {
/// Permiso efectivo: el declarado, o [`AdminPermission::Access`] si no se especificó ninguno.
pub fn permission_key(&self) -> PermissionRef {
self.permission.unwrap_or(&AdminPermission::Access)
}
/// Devuelve `true` si el usuario actual puede acceder a esta página. /// Devuelve `true` si el usuario actual puede acceder a esta página.
pub fn is_accessible(&self, cx: &Context) -> bool { pub fn is_accessible(&self, cx: &Context) -> bool {
cx.request() cx.request()
.is_some_and(|request| has_permission(request, self.permission_key())) .is_some_and(|request| has_permission(request, self.permission))
} }
} }
@ -277,11 +233,9 @@ pub fn global() -> &'static AdminRegistry {
pub fn can_access_admin(cx: &Context) -> bool { pub fn can_access_admin(cx: &Context) -> bool {
let reg = global(); let reg = global();
reg.ordered_sections().into_iter().any(|section| { reg.ordered_sections().into_iter().any(|section| {
section.is_visible(cx) reg.pages_for_section(&section.key)
&& reg .into_iter()
.pages_for_section(&section.key) .any(|page| page.is_accessible(cx))
.into_iter()
.any(|page| page.is_accessible(cx))
}) })
} }
@ -306,9 +260,6 @@ pub fn admin_navbar(cx: &Context) -> Navbar {
)); ));
for section in reg.ordered_sections() { for section in reg.ordered_sections() {
if !section.is_visible(cx) {
continue;
}
let pages: Vec<_> = reg let pages: Vec<_> = reg
.pages_for_section(&section.key) .pages_for_section(&section.key)
.into_iter() .into_iter()

View file

@ -3,7 +3,7 @@ use pagetop::prelude::*;
use crate::ADMIN_BASE_PATH; use crate::ADMIN_BASE_PATH;
use crate::LOCALES_ADMIN; use crate::LOCALES_ADMIN;
use crate::action::SectionBag; use crate::action::SectionBag;
use crate::registry::{AdminPermission, AdminSection}; use crate::registry::AdminSection;
/// Declara las secciones incorporadas del panel de administración. /// Declara las secciones incorporadas del panel de administración.
/// ///
@ -16,35 +16,30 @@ pub(crate) fn declare_default_sections(bag: &mut SectionBag) {
key: "people".to_owned(), key: "people".to_owned(),
path: format!("{}/people", base), path: format!("{}/people", base),
title: Lc::t("section-people", &LOCALES_ADMIN), title: Lc::t("section-people", &LOCALES_ADMIN),
permission: Some(&AdminPermission::AccessPeople),
weight: 10, weight: 10,
}); });
bag.add(AdminSection { bag.add(AdminSection {
key: "structure".to_owned(), key: "structure".to_owned(),
path: format!("{}/structure", base), path: format!("{}/structure", base),
title: Lc::t("section-structure", &LOCALES_ADMIN), title: Lc::t("section-structure", &LOCALES_ADMIN),
permission: Some(&AdminPermission::AccessStructure),
weight: 20, weight: 20,
}); });
bag.add(AdminSection { bag.add(AdminSection {
key: "config".to_owned(), key: "config".to_owned(),
path: format!("{}/config", base), path: format!("{}/config", base),
title: Lc::t("section-config", &LOCALES_ADMIN), title: Lc::t("section-config", &LOCALES_ADMIN),
permission: Some(&AdminPermission::AccessConfig),
weight: 30, weight: 30,
}); });
bag.add(AdminSection { bag.add(AdminSection {
key: "reports".to_owned(), key: "reports".to_owned(),
path: format!("{}/reports", base), path: format!("{}/reports", base),
title: Lc::t("section-reports", &LOCALES_ADMIN), title: Lc::t("section-reports", &LOCALES_ADMIN),
permission: Some(&AdminPermission::AccessReports),
weight: 40, weight: 40,
}); });
bag.add(AdminSection { bag.add(AdminSection {
key: "help".to_owned(), key: "help".to_owned(),
path: format!("{}/help", base), path: format!("{}/help", base),
title: Lc::t("section-help", &LOCALES_ADMIN), title: Lc::t("section-help", &LOCALES_ADMIN),
permission: None,
weight: 50, weight: 50,
}); });
} }

View file

@ -155,7 +155,7 @@ fn declare_admin_pages(bag: &mut PageBag) {
title: Lc::t("title-admin-users", &LOCALES_USER), title: Lc::t("title-admin-users", &LOCALES_USER),
description: Some(Lc::t("description-admin-users", &LOCALES_USER)), description: Some(Lc::t("description-admin-users", &LOCALES_USER)),
weight: 0, weight: 0,
permission: Some(&permission::UserPermission::AdminUsers), permission: &permission::UserPermission::AdminUsers,
kind: AdminPageKind::View, kind: AdminPageKind::View,
}); });
bag.add(AdminPage { bag.add(AdminPage {
@ -164,7 +164,7 @@ fn declare_admin_pages(bag: &mut PageBag) {
title: Lc::t("title-admin-roles", &LOCALES_USER), title: Lc::t("title-admin-roles", &LOCALES_USER),
description: Some(Lc::t("description-admin-roles", &LOCALES_USER)), description: Some(Lc::t("description-admin-roles", &LOCALES_USER)),
weight: 10, weight: 10,
permission: Some(&permission::UserPermission::AdminRoles), permission: &permission::UserPermission::AdminRoles,
kind: AdminPageKind::View, kind: AdminPageKind::View,
}); });
bag.add(AdminPage { bag.add(AdminPage {
@ -173,7 +173,7 @@ fn declare_admin_pages(bag: &mut PageBag) {
title: Lc::t("title-admin-permissions", &LOCALES_USER), title: Lc::t("title-admin-permissions", &LOCALES_USER),
description: Some(Lc::t("description-admin-permissions", &LOCALES_USER)), description: Some(Lc::t("description-admin-permissions", &LOCALES_USER)),
weight: 20, weight: 20,
permission: Some(&permission::UserPermission::AdminPermissions), permission: &permission::UserPermission::AdminPermissions,
kind: AdminPageKind::View, kind: AdminPageKind::View,
}); });
} }