diff --git a/server b/server index 4759f130..9ad620a7 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 4759f1308d139d5eab533f6eca4e4549379a4f70 +Subproject commit 9ad620a72fe31291df4f0d538d33cd79c7f321e0 diff --git a/tui-canvas b/tui-canvas index f9d2a7a3..7c377546 160000 --- a/tui-canvas +++ b/tui-canvas @@ -1 +1 @@ -Subproject commit f9d2a7a3121abe68aaa232624035539eaf029240 +Subproject commit 7c3775469ed101bbe0e5731fecb9b1f70867f7c0 diff --git a/web/locales/cs/main.ftl b/web/locales/cs/main.ftl index 95100fc9..0983057a 100644 --- a/web/locales/cs/main.ftl +++ b/web/locales/cs/main.ftl @@ -109,6 +109,7 @@ admin-add-table = Přidat tabulku admin-add-logic = Přidat logiku admin-add-validation = Přidat validaci admin-add-rule = Přidat pravidlo +admin-import = Import admin-export = Export admin-exchange-rates = Devizové kurzy admin-unavailable-title = Administrační panel není dostupný diff --git a/web/locales/en/main.ftl b/web/locales/en/main.ftl index 493aaad7..74d76c3c 100644 --- a/web/locales/en/main.ftl +++ b/web/locales/en/main.ftl @@ -114,6 +114,7 @@ admin-add-table = Add table admin-add-logic = Add logic admin-add-validation = Add validation admin-add-rule = Add rule +admin-import = Import admin-export = Export admin-exchange-rates = Exchange rates admin-unavailable-title = Admin panel unavailable diff --git a/web/locales/sk/main.ftl b/web/locales/sk/main.ftl index 24a3cce5..a97eaac8 100644 --- a/web/locales/sk/main.ftl +++ b/web/locales/sk/main.ftl @@ -109,6 +109,7 @@ admin-add-table = Pridať tabuľku admin-add-logic = Pridať logiku admin-add-validation = Pridať validáciu admin-add-rule = Pridať pravidlo +admin-import = Import admin-export = Export admin-exchange-rates = Devízové kurzy admin-unavailable-title = Administračný panel nie je dostupný diff --git a/web/src/authz.rs b/web/src/authz.rs index 6eea912c..7ef937e9 100644 --- a/web/src/authz.rs +++ b/web/src/authz.rs @@ -14,6 +14,9 @@ pub(crate) const READ: &str = "read"; /// per-profile question. pub(crate) const ALL_ECB: &str = "ecb:*"; +/// Mirrors `server/src/auth/rbac/roles.rs`. +pub(crate) const SUPERADMIN: &str = "superadmin"; + pub(crate) fn permits(snapshot: &AuthorizationSnapshot, object: &str, action: &str) -> bool { permissions_permit(&snapshot.permissions, object, action) } @@ -38,6 +41,14 @@ pub(crate) fn can_read_ecb(snapshot: &AuthorizationSnapshot) -> bool { permits(snapshot, ALL_ECB, READ) } +/// Whether the caller is the superadmin. Bulk data transfer — CSV import and +/// export — is theirs alone: it moves whole tables at once, which is the same +/// reach as the backup they already hold, and a data role's own grants say +/// nothing about whether it should have that. +pub(crate) fn is_superadmin(snapshot: &AuthorizationSnapshot) -> bool { + snapshot.role == SUPERADMIN +} + pub(crate) fn can_open_admin(snapshot: &AuthorizationSnapshot) -> bool { [ STRUCT_PROFILE, diff --git a/web/src/pages/admin/admin/loader.rs b/web/src/pages/admin/admin/loader.rs index a2245157..5d2500f5 100644 --- a/web/src/pages/admin/admin/loader.rs +++ b/web/src/pages/admin/admin/loader.rs @@ -175,9 +175,8 @@ pub(crate) async fn load_admin_page( can_manage_tables: crate::authz::can_manage(&authorization, crate::authz::STRUCT_TABLE), can_manage_scripts: crate::authz::can_manage(&authorization, crate::authz::STRUCT_SCRIPT), can_manage_validations: crate::authz::can_manage(&authorization, crate::authz::STRUCT_VALIDATION), - can_export: authorization.permissions.iter().any(|permission| { - permission.action == "read" && permission.object.starts_with("data:") - }), + can_import: crate::authz::is_superadmin(&authorization), + can_export: crate::authz::is_superadmin(&authorization), can_ecb: crate::authz::can_read_ecb(&authorization), }) } diff --git a/web/src/pages/admin/admin/state.rs b/web/src/pages/admin/admin/state.rs index 5d574212..cd4452be 100644 --- a/web/src/pages/admin/admin/state.rs +++ b/web/src/pages/admin/admin/state.rs @@ -17,6 +17,10 @@ pub(crate) struct AdminPageState { pub can_manage_tables: bool, pub can_manage_scripts: bool, pub can_manage_validations: bool, + /// The two transfer pages are separate actions with separate permissions: + /// loading rows needs `insert`, taking them out needs `read`, and a role + /// with one of them is not offered the other. + pub can_import: bool, pub can_export: bool, /// Whether the exchange-rate pipeline is visible to this caller. Not a /// structural area: it is granted through the ECB object, like the diff --git a/web/src/pages/admin/admin/ui.rs b/web/src/pages/admin/admin/ui.rs index 71d74088..2b10b98a 100644 --- a/web/src/pages/admin/admin/ui.rs +++ b/web/src/pages/admin/admin/ui.rs @@ -72,6 +72,7 @@ mod tests { can_manage_tables: true, can_manage_scripts: true, can_manage_validations: true, + can_import: true, can_export: true, can_ecb: true, }; @@ -81,6 +82,7 @@ mod tests { "/admin/logic/new", "/admin/validation/new", "/admin/validation/sets/new", + "/admin/import", "/admin/export", "/logout", ] { @@ -115,6 +117,7 @@ mod tests { can_manage_tables: true, can_manage_scripts: true, can_manage_validations: true, + can_import: true, can_export: true, can_ecb: true, }; @@ -150,6 +153,7 @@ mod tests { can_manage_tables: true, can_manage_scripts: true, can_manage_validations: true, + can_import: true, can_export: true, can_ecb: true, }; @@ -188,6 +192,7 @@ mod tests { can_manage_tables: true, can_manage_scripts: true, can_manage_validations: true, + can_import: true, can_export: true, can_ecb: true, }; @@ -225,6 +230,7 @@ mod tests { can_manage_tables: true, can_manage_scripts: true, can_manage_validations: true, + can_import: true, can_export: true, can_ecb: true, }; diff --git a/web/src/pages/import_export/common/loader.rs b/web/src/pages/import_export/common/loader.rs index da856893..af2ea853 100644 --- a/web/src/pages/import_export/common/loader.rs +++ b/web/src/pages/import_export/common/loader.rs @@ -41,10 +41,9 @@ pub(crate) async fn load_catalog( _ => LoadError::Backend(error.message().to_string()), })? .into_inner(); - let has_required_permission = authorization.permissions.iter().any(|permission| { - permission.action == required_action && permission.object.starts_with("data:") - }); - if !has_required_permission { + // Hiding the link is not the check: the route is refused here too, so a + // typed URL gets the same answer as a missing button. + if !crate::authz::is_superadmin(&authorization) { return Err(LoadError::Forbidden); } let mut definitions = state.definitions; diff --git a/web/src/ui/mod.rs b/web/src/ui/mod.rs index dcc9a467..e54fabd8 100644 --- a/web/src/ui/mod.rs +++ b/web/src/ui/mod.rs @@ -154,12 +154,10 @@ impl Nav { // panel: managing either roles or users is enough to open it. nav.can_permissions = crate::authz::can_manage(authorization, crate::authz::STRUCT_ROLE) || crate::authz::can_manage(authorization, crate::authz::STRUCT_USER); - nav.can_import = authorization.permissions.iter().any(|permission| { - permission.action == "insert" && permission.object.starts_with("data:") - }); - nav.can_export = authorization.permissions.iter().any(|permission| { - permission.action == "read" && permission.object.starts_with("data:") - }); + // Both transfer pages are the superadmin's — see + // `crate::authz::is_superadmin`. + nav.can_import = crate::authz::is_superadmin(authorization); + nav.can_export = nav.can_import; nav.can_ecb = crate::authz::can_read_ecb(authorization); nav } diff --git a/web/templates/pages/admin/admin/admin.html b/web/templates/pages/admin/admin/admin.html index d61f50e4..502ca15d 100644 --- a/web/templates/pages/admin/admin/admin.html +++ b/web/templates/pages/admin/admin/admin.html @@ -16,6 +16,7 @@ {% if page.can_manage_scripts %}{{ nav.tr("admin-add-logic") }}{% endif %} {% if page.can_manage_validations %}{{ nav.tr("admin-add-validation") }}{% endif %} {% if page.can_manage_validations %}{{ nav.tr("admin-add-rule") }}{% endif %} + {% if page.can_import %}{{ nav.tr("admin-import") }}{% endif %} {% if page.can_export %}{{ nav.tr("admin-export") }}{% endif %} {% if page.can_ecb %}{{ nav.tr("admin-exchange-rates") }}{% endif %}