import export rbac

This commit is contained in:
Priec
2026-08-15 19:33:18 +02:00
parent e3104a367e
commit e15b2681fd
12 changed files with 36 additions and 15 deletions

View File

@@ -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ý

View File

@@ -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

View File

@@ -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ý

View File

@@ -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,

View File

@@ -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),
})
}

View File

@@ -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

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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
}

View File

@@ -16,6 +16,7 @@
{% if page.can_manage_scripts %}<a href="/admin/logic/new">{{ nav.tr("admin-add-logic") }}</a>{% endif %}
{% if page.can_manage_validations %}<a href="/admin/validation/new">{{ nav.tr("admin-add-validation") }}</a>{% endif %}
{% if page.can_manage_validations %}<a href="/admin/validation/sets/new">{{ nav.tr("admin-add-rule") }}</a>{% endif %}
{% if page.can_import %}<a href="/admin/import">{{ nav.tr("admin-import") }}</a>{% endif %}
{% if page.can_export %}<a href="/admin/export">{{ nav.tr("admin-export") }}</a>{% endif %}
{% if page.can_ecb %}<a href="/admin/ecb">{{ nav.tr("admin-exchange-rates") }}</a>{% endif %}
</div>