import export rbac2
This commit is contained in:
2
server
2
server
Submodule server updated: 9ad620a72f...227c4103e9
@@ -604,7 +604,7 @@ import-err-header-lengths = Řádek hlavičky tabulek a sloupců mají různou d
|
|||||||
import-err-row-width = Řádek dat CSV má jiný počet polí než hlavička.
|
import-err-row-width = Řádek dat CSV má jiný počet polí než hlavička.
|
||||||
import-err-no-data-rows = CSV obsahuje hlavičky, ale žádné řádky dat.
|
import-err-no-data-rows = CSV obsahuje hlavičky, ale žádné řádky dat.
|
||||||
import-err-missing-type = Chybí typ sloupce '{ $column }'
|
import-err-missing-type = Chybí typ sloupce '{ $column }'
|
||||||
import-err-permission = Vyžaduje se oprávnění k vkládání pro alespoň jednu čitelnou tabulku.
|
import-err-permission = Vyžaduje se oprávnění k importu. Tabulka navíc potřebuje oprávnění k vkládání, aby do ní šlo načítat.
|
||||||
import-err-unterminated-quote = CSV obsahuje neuzavřenou uvozovkovou hodnotu.
|
import-err-unterminated-quote = CSV obsahuje neuzavřenou uvozovkovou hodnotu.
|
||||||
import-err-empty = CSV je prázdné.
|
import-err-empty = CSV je prázdné.
|
||||||
import-err-bad-boolean = Neplatná booleovská hodnota '{ $value }'
|
import-err-bad-boolean = Neplatná booleovská hodnota '{ $value }'
|
||||||
@@ -627,7 +627,7 @@ export-err-tables-not-in-profile = Tabulka nepatří do vybraného rozsahu.
|
|||||||
export-err-missing-structure = Backend vynechal požadovanou strukturu tabulky.
|
export-err-missing-structure = Backend vynechal požadovanou strukturu tabulky.
|
||||||
export-err-negative-count = Backend vrátil záporný počet řádků.
|
export-err-negative-count = Backend vrátil záporný počet řádků.
|
||||||
export-err-range = Export přesahuje podporovaný rozsah pozic řádků.
|
export-err-range = Export přesahuje podporovaný rozsah pozic řádků.
|
||||||
export-err-permission = Vyžaduje se oprávnění ke čtení pro alespoň jednu tabulku.
|
export-err-permission = Vyžaduje se oprávnění k exportu. Tabulka navíc potřebuje oprávnění ke čtení, aby ji šlo stáhnout.
|
||||||
|
|
||||||
# --- Přidat logiku ----------------------------------------------------------------
|
# --- Přidat logiku ----------------------------------------------------------------
|
||||||
add-logic-title = Přidat logiku
|
add-logic-title = Přidat logiku
|
||||||
|
|||||||
@@ -591,7 +591,7 @@ import-err-header-lengths = Table and column header rows have different lengths.
|
|||||||
import-err-row-width = A CSV data row has a different number of fields than the header.
|
import-err-row-width = A CSV data row has a different number of fields than the header.
|
||||||
import-err-no-data-rows = CSV contains headers but no data rows.
|
import-err-no-data-rows = CSV contains headers but no data rows.
|
||||||
import-err-missing-type = Missing type for column '{ $column }'
|
import-err-missing-type = Missing type for column '{ $column }'
|
||||||
import-err-permission = Insert permission is required for at least one readable table.
|
import-err-permission = Import permission is required. A table also needs insert permission before it can be loaded.
|
||||||
import-err-unterminated-quote = CSV contains an unterminated quoted value.
|
import-err-unterminated-quote = CSV contains an unterminated quoted value.
|
||||||
import-err-empty = CSV is empty.
|
import-err-empty = CSV is empty.
|
||||||
import-err-bad-boolean = Invalid boolean value '{ $value }'
|
import-err-bad-boolean = Invalid boolean value '{ $value }'
|
||||||
@@ -614,7 +614,7 @@ export-err-tables-not-in-profile = The table does not belong to the selected sco
|
|||||||
export-err-missing-structure = The backend omitted a requested table structure.
|
export-err-missing-structure = The backend omitted a requested table structure.
|
||||||
export-err-negative-count = The backend returned a negative row count.
|
export-err-negative-count = The backend returned a negative row count.
|
||||||
export-err-range = The export exceeds the supported row-position range.
|
export-err-range = The export exceeds the supported row-position range.
|
||||||
export-err-permission = Read permission is required for at least one table.
|
export-err-permission = Export permission is required. A table also needs read permission before it can be downloaded.
|
||||||
|
|
||||||
# --- Add logic -------------------------------------------------------------
|
# --- Add logic -------------------------------------------------------------
|
||||||
add-logic-title = Add logic
|
add-logic-title = Add logic
|
||||||
|
|||||||
@@ -604,7 +604,7 @@ import-err-header-lengths = Riadok hlavičky tabuliek a stĺpcov majú rôznu d
|
|||||||
import-err-row-width = Riadok údajov CSV má iný počet polí ako hlavička.
|
import-err-row-width = Riadok údajov CSV má iný počet polí ako hlavička.
|
||||||
import-err-no-data-rows = CSV obsahuje hlavičky, ale žiadne riadky údajov.
|
import-err-no-data-rows = CSV obsahuje hlavičky, ale žiadne riadky údajov.
|
||||||
import-err-missing-type = Chýba typ stĺpca '{ $column }'
|
import-err-missing-type = Chýba typ stĺpca '{ $column }'
|
||||||
import-err-permission = Vyžaduje sa oprávnenie na vkladanie pre aspoň jednu čitateľnú tabuľku.
|
import-err-permission = Vyžaduje sa oprávnenie na import. Tabuľka navyše potrebuje oprávnenie na vkladanie, aby sa do nej dalo načítať.
|
||||||
import-err-unterminated-quote = CSV obsahuje neuzavretú úvodzovkovú hodnotu.
|
import-err-unterminated-quote = CSV obsahuje neuzavretú úvodzovkovú hodnotu.
|
||||||
import-err-empty = CSV je prázdny.
|
import-err-empty = CSV je prázdny.
|
||||||
import-err-bad-boolean = Neplatná boolovská hodnota '{ $value }'
|
import-err-bad-boolean = Neplatná boolovská hodnota '{ $value }'
|
||||||
@@ -627,7 +627,7 @@ export-err-tables-not-in-profile = Tabuľka nepatrí do vybraného rozsahu.
|
|||||||
export-err-missing-structure = Backend vynechal požadovanú štruktúru tabuľky.
|
export-err-missing-structure = Backend vynechal požadovanú štruktúru tabuľky.
|
||||||
export-err-negative-count = Backend vrátil záporný počet riadkov.
|
export-err-negative-count = Backend vrátil záporný počet riadkov.
|
||||||
export-err-range = Export presahuje podporovaný rozsah pozícií riadkov.
|
export-err-range = Export presahuje podporovaný rozsah pozícií riadkov.
|
||||||
export-err-permission = Vyžaduje sa oprávnenie na čítanie pre aspoň jednu tabuľku.
|
export-err-permission = Vyžaduje sa oprávnenie na export. Tabuľka navyše potrebuje oprávnenie na čítanie, aby sa dala stiahnuť.
|
||||||
|
|
||||||
# --- Pridať logiku ------------------------------------------------------------
|
# --- Pridať logiku ------------------------------------------------------------
|
||||||
add-logic-title = Pridať logiku
|
add-logic-title = Pridať logiku
|
||||||
|
|||||||
@@ -14,8 +14,14 @@ pub(crate) const READ: &str = "read";
|
|||||||
/// per-profile question.
|
/// per-profile question.
|
||||||
pub(crate) const ALL_ECB: &str = "ecb:*";
|
pub(crate) const ALL_ECB: &str = "ecb:*";
|
||||||
|
|
||||||
/// Mirrors `server/src/auth/rbac/roles.rs`.
|
/// The two bulk-transfer actions, mirroring
|
||||||
pub(crate) const SUPERADMIN: &str = "superadmin";
|
/// `server/src/auth/rbac/objects.rs`. Taking a whole table out as a file is
|
||||||
|
/// its own grant rather than something `read` implies, and loading one in is
|
||||||
|
/// its own grant rather than something `insert` implies — so a superadmin can
|
||||||
|
/// hand a role the row access without the bulk transfer, or the other way
|
||||||
|
/// round.
|
||||||
|
pub(crate) const EXPORT: &str = "export";
|
||||||
|
pub(crate) const IMPORT: &str = "import";
|
||||||
|
|
||||||
pub(crate) fn permits(snapshot: &AuthorizationSnapshot, object: &str, action: &str) -> bool {
|
pub(crate) fn permits(snapshot: &AuthorizationSnapshot, object: &str, action: &str) -> bool {
|
||||||
permissions_permit(&snapshot.permissions, object, action)
|
permissions_permit(&snapshot.permissions, object, action)
|
||||||
@@ -41,12 +47,15 @@ pub(crate) fn can_read_ecb(snapshot: &AuthorizationSnapshot) -> bool {
|
|||||||
permits(snapshot, ALL_ECB, READ)
|
permits(snapshot, ALL_ECB, READ)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether the caller is the superadmin. Bulk data transfer — CSV import and
|
/// Whether the caller holds a transfer action on any table at all — what the
|
||||||
/// export — is theirs alone: it moves whole tables at once, which is the same
|
/// Import and Export links are shown for. Which tables those are is decided
|
||||||
/// reach as the backup they already hold, and a data role's own grants say
|
/// per table by [`permits_table`], since a grant may name one table, one
|
||||||
/// nothing about whether it should have that.
|
/// profile, or everything.
|
||||||
pub(crate) fn is_superadmin(snapshot: &AuthorizationSnapshot) -> bool {
|
pub(crate) fn can_transfer_anything(snapshot: &AuthorizationSnapshot, action: &str) -> bool {
|
||||||
snapshot.role == SUPERADMIN
|
snapshot
|
||||||
|
.permissions
|
||||||
|
.iter()
|
||||||
|
.any(|permission| permission.action == action && permission.object.starts_with("data:"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn can_open_admin(snapshot: &AuthorizationSnapshot) -> bool {
|
pub(crate) fn can_open_admin(snapshot: &AuthorizationSnapshot) -> bool {
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ pub(crate) async fn load_admin_page(
|
|||||||
can_manage_tables: crate::authz::can_manage(&authorization, crate::authz::STRUCT_TABLE),
|
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_scripts: crate::authz::can_manage(&authorization, crate::authz::STRUCT_SCRIPT),
|
||||||
can_manage_validations: crate::authz::can_manage(&authorization, crate::authz::STRUCT_VALIDATION),
|
can_manage_validations: crate::authz::can_manage(&authorization, crate::authz::STRUCT_VALIDATION),
|
||||||
can_import: crate::authz::is_superadmin(&authorization),
|
can_import: crate::authz::can_transfer_anything(&authorization, crate::authz::IMPORT),
|
||||||
can_export: crate::authz::is_superadmin(&authorization),
|
can_export: crate::authz::can_transfer_anything(&authorization, crate::authz::EXPORT),
|
||||||
can_ecb: crate::authz::can_read_ecb(&authorization),
|
can_ecb: crate::authz::can_read_ecb(&authorization),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,19 @@ pub(crate) struct Profile {
|
|||||||
pub tables: Vec<String>,
|
pub tables: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The tables one transfer page may offer.
|
||||||
|
///
|
||||||
|
/// `transfer_action` is the page's own grant — `export` or `import`.
|
||||||
|
/// `row_action` is what the transfer does underneath, `read` or `insert`, and
|
||||||
|
/// it is checked as well: the backend authorizes the row calls the transfer
|
||||||
|
/// makes, so a table offered on the strength of the transfer grant alone would
|
||||||
|
/// be refused halfway through the download. A table appears here only when the
|
||||||
|
/// role holds both.
|
||||||
pub(crate) async fn load_catalog(
|
pub(crate) async fn load_catalog(
|
||||||
state: AppState,
|
state: AppState,
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
required_action: &str,
|
transfer_action: &str,
|
||||||
|
row_action: &str,
|
||||||
) -> Result<Catalog, LoadError> {
|
) -> Result<Catalog, LoadError> {
|
||||||
let request = authenticated_request(headers, GetAuthorizationRequest {})
|
let request = authenticated_request(headers, GetAuthorizationRequest {})
|
||||||
.map_err(|_| LoadError::Unauthenticated)?;
|
.map_err(|_| LoadError::Unauthenticated)?;
|
||||||
@@ -43,9 +52,13 @@ pub(crate) async fn load_catalog(
|
|||||||
.into_inner();
|
.into_inner();
|
||||||
// Hiding the link is not the check: the route is refused here too, so a
|
// 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.
|
// typed URL gets the same answer as a missing button.
|
||||||
if !crate::authz::is_superadmin(&authorization) {
|
if !crate::authz::can_transfer_anything(&authorization, transfer_action) {
|
||||||
return Err(LoadError::Forbidden);
|
return Err(LoadError::Forbidden);
|
||||||
}
|
}
|
||||||
|
let permits = |profile: &str, table: &str| {
|
||||||
|
crate::authz::permits_table(&authorization, profile, table, transfer_action)
|
||||||
|
&& crate::authz::permits_table(&authorization, profile, table, row_action)
|
||||||
|
};
|
||||||
let mut definitions = state.definitions;
|
let mut definitions = state.definitions;
|
||||||
let tree = definitions
|
let tree = definitions
|
||||||
.get_profile_tree(authenticated_request(headers, Empty {}).map_err(|_| LoadError::Unauthenticated)?)
|
.get_profile_tree(authenticated_request(headers, Empty {}).map_err(|_| LoadError::Unauthenticated)?)
|
||||||
@@ -72,9 +85,7 @@ pub(crate) async fn load_catalog(
|
|||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|table| table.name)
|
.map(|table| table.name)
|
||||||
.filter(|table| {
|
.filter(|table| permits(GLOBAL_SCOPE, table))
|
||||||
crate::authz::permits_table(&authorization, GLOBAL_SCOPE, table, required_action)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let locale = Locale::from_headers(headers);
|
let locale = Locale::from_headers(headers);
|
||||||
@@ -91,14 +102,7 @@ pub(crate) async fn load_catalog(
|
|||||||
.tables
|
.tables
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|table| !table.global)
|
.filter(|table| !table.global)
|
||||||
.filter(|table| {
|
.filter(|table| permits(&profile.name, &table.name))
|
||||||
crate::authz::permits_table(
|
|
||||||
&authorization,
|
|
||||||
&profile.name,
|
|
||||||
&table.name,
|
|
||||||
required_action,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.map(|table| table.name)
|
.map(|table| table.name)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
(!tables.is_empty()).then_some(Profile {
|
(!tables.is_empty()).then_some(Profile {
|
||||||
@@ -108,15 +112,15 @@ pub(crate) async fn load_catalog(
|
|||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// An insert-only role is deliberately absent from GetProfileTree because
|
// A role that may import but not read is deliberately absent from
|
||||||
// that listing is filtered by read permission. Exact table grants still
|
// GetProfileTree, because that listing is filtered by read permission.
|
||||||
// carry enough information to offer their target here. A wildcard-only
|
// Exact table grants still carry enough information to offer their target
|
||||||
// insert role has no table name to offer, so its scope stays empty and the
|
// here. A wildcard-only grant has no table name to offer, so its scope
|
||||||
// backend remains the authoritative permission check.
|
// stays empty and the backend remains the authoritative permission check.
|
||||||
for permission in authorization
|
for permission in authorization
|
||||||
.permissions
|
.permissions
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|permission| permission.action == required_action)
|
.filter(|permission| permission.action == transfer_action)
|
||||||
{
|
{
|
||||||
let Some(target) = permission.object.strip_prefix("data:") else {
|
let Some(target) = permission.object.strip_prefix("data:") else {
|
||||||
continue;
|
continue;
|
||||||
@@ -127,6 +131,9 @@ pub(crate) async fn load_catalog(
|
|||||||
if profile_name == "*" || table_name == "*" {
|
if profile_name == "*" || table_name == "*" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if !permits(profile_name, table_name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if let Some(profile) = profiles
|
if let Some(profile) = profiles
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|profile| profile.name == profile_name)
|
.find(|profile| profile.name == profile_name)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ pub(crate) async fn load_page(
|
|||||||
state: AppState,
|
state: AppState,
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
) -> Result<ExportPageState, LoadError> {
|
) -> Result<ExportPageState, LoadError> {
|
||||||
let catalog = load_catalog(state, headers, "read").await?;
|
let catalog = load_catalog(state, headers, crate::authz::EXPORT, "read").await?;
|
||||||
Ok(ExportPageState {
|
Ok(ExportPageState {
|
||||||
nav: crate::ui::Nav::from_authorization(headers, "", &catalog.authorization),
|
nav: crate::ui::Nav::from_authorization(headers, "", &catalog.authorization),
|
||||||
catalog,
|
catalog,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ pub(crate) async fn load_page(
|
|||||||
form: ImportForm,
|
form: ImportForm,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
) -> Result<ImportPageState, LoadError> {
|
) -> Result<ImportPageState, LoadError> {
|
||||||
let catalog = load_catalog(state, headers, "insert").await?;
|
let catalog = load_catalog(state, headers, crate::authz::IMPORT, "insert").await?;
|
||||||
Ok(ImportPageState {
|
Ok(ImportPageState {
|
||||||
nav: crate::ui::Nav::from_authorization(headers, "", &catalog.authorization),
|
nav: crate::ui::Nav::from_authorization(headers, "", &catalog.authorization),
|
||||||
catalog,
|
catalog,
|
||||||
|
|||||||
@@ -4,7 +4,14 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// The columns of every grant matrix, in the order the server lists them.
|
/// The columns of every grant matrix, in the order the server lists them.
|
||||||
pub(crate) const ACTIONS: [&str; 4] = ["read", "insert", "update", "delete"];
|
pub(crate) const ACTIONS: [&str; 6] = [
|
||||||
|
"read",
|
||||||
|
"insert",
|
||||||
|
"update",
|
||||||
|
"delete",
|
||||||
|
"export",
|
||||||
|
"import",
|
||||||
|
];
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
#[derive(Clone, Debug, Default, serde::Deserialize)]
|
||||||
pub(crate) struct Selection {
|
pub(crate) struct Selection {
|
||||||
@@ -81,7 +88,7 @@ pub(crate) struct Cell {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GrantsPage {
|
impl GrantsPage {
|
||||||
pub(crate) fn actions(&self) -> [&'static str; 4] {
|
pub(crate) fn actions(&self) -> [&'static str; ACTIONS.len()] {
|
||||||
ACTIONS
|
ACTIONS
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,6 +257,8 @@ mod tests {
|
|||||||
"data:acme/*|insert",
|
"data:acme/*|insert",
|
||||||
"data:acme/*|update",
|
"data:acme/*|update",
|
||||||
"data:acme/*|delete",
|
"data:acme/*|delete",
|
||||||
|
"data:acme/*|export",
|
||||||
|
"data:acme/*|import",
|
||||||
"ecb:acme|read",
|
"ecb:acme|read",
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ pub(crate) fn starter_grants(
|
|||||||
grants.push((object, action));
|
grants.push((object, action));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Bulk transfer applies to tables, not to journals, which the
|
||||||
|
// transfer pages never offer.
|
||||||
|
grants.push(("data:*", "export"));
|
||||||
|
grants.push(("data:*", "import"));
|
||||||
Ok(grants)
|
Ok(grants)
|
||||||
}
|
}
|
||||||
other => Err(tr!(
|
other => Err(tr!(
|
||||||
@@ -122,7 +126,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(starter_grants(locale, "read").unwrap().len(), 3);
|
assert_eq!(starter_grants(locale, "read").unwrap().len(), 3);
|
||||||
assert_eq!(starter_grants(locale, "full").unwrap().len(), 9);
|
// Four row actions on two wildcard objects, ECB read, and the two
|
||||||
|
// transfer actions, which apply to tables alone.
|
||||||
|
assert_eq!(starter_grants(locale, "full").unwrap().len(), 11);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -154,10 +154,10 @@ impl Nav {
|
|||||||
// panel: managing either roles or users is enough to open it.
|
// panel: managing either roles or users is enough to open it.
|
||||||
nav.can_permissions = crate::authz::can_manage(authorization, crate::authz::STRUCT_ROLE)
|
nav.can_permissions = crate::authz::can_manage(authorization, crate::authz::STRUCT_ROLE)
|
||||||
|| crate::authz::can_manage(authorization, crate::authz::STRUCT_USER);
|
|| crate::authz::can_manage(authorization, crate::authz::STRUCT_USER);
|
||||||
// Both transfer pages are the superadmin's — see
|
nav.can_import =
|
||||||
// `crate::authz::is_superadmin`.
|
crate::authz::can_transfer_anything(authorization, crate::authz::IMPORT);
|
||||||
nav.can_import = crate::authz::is_superadmin(authorization);
|
nav.can_export =
|
||||||
nav.can_export = nav.can_import;
|
crate::authz::can_transfer_anything(authorization, crate::authz::EXPORT);
|
||||||
nav.can_ecb = crate::authz::can_read_ecb(authorization);
|
nav.can_ecb = crate::authz::can_read_ecb(authorization);
|
||||||
nav
|
nav
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user