diff --git a/server b/server index b7504c89..645b45e8 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit b7504c894c2f3a962c289a03bff284e840747efc +Subproject commit 645b45e81ba80067ff2aa1d98a2c8ef9cd603b07 diff --git a/web/src/authz.rs b/web/src/authz.rs index 1de37cf7..cbae399b 100644 --- a/web/src/authz.rs +++ b/web/src/authz.rs @@ -19,7 +19,8 @@ pub(crate) const ALL_ECB: &str = "ecb:*"; /// 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. +/// round. `import` is the whole authorization for the bulk endpoint: a role +/// that holds it may load a file without holding `insert`. pub(crate) const EXPORT: &str = "export"; pub(crate) const IMPORT: &str = "import"; @@ -146,6 +147,25 @@ mod tests { assert!(!outranks("sales", "clerk")); } + /// The transfer links are shown on the grant and nothing else. A role + /// invented this morning holds them exactly as superadmin does, and a role + /// with every structural area but no transfer grant sees neither link. + #[test] + fn the_transfer_links_follow_the_grant_not_the_role_name() { + let warehouse = AuthorizationSnapshot { + role: "warehouse-night-shift".to_string(), + ..snapshot(&[("data:acme/stock", IMPORT)]) + }; + assert!(can_transfer_anything(&warehouse, IMPORT)); + assert!(!can_transfer_anything(&warehouse, EXPORT)); + assert!(permits_table(&warehouse, "acme", "stock", IMPORT)); + assert!(!permits_table(&warehouse, "acme", "invoice", IMPORT)); + + let structural = snapshot(&[(STRUCT_TABLE, MANAGE), (STRUCT_ROLE, MANAGE)]); + assert!(!can_transfer_anything(&structural, IMPORT)); + assert!(!can_transfer_anything(&structural, EXPORT)); + } + #[test] fn data_wildcards_match_the_server_object_shapes() { let global = snapshot(&[("data:*", "read")]); diff --git a/web/src/pages/import_export/common/loader.rs b/web/src/pages/import_export/common/loader.rs index 1d6c6479..715b3b9f 100644 --- a/web/src/pages/import_export/common/loader.rs +++ b/web/src/pages/import_export/common/loader.rs @@ -29,16 +29,18 @@ pub(crate) struct Profile { /// 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. +/// +/// `row_action` is a second grant to require, for a page whose work the backend +/// authorizes as ordinary row calls: an export downloads by reading rows, so it +/// passes `read` and a table offered on the strength of `export` alone would be +/// refused halfway through the download. An import passes `None` — the bulk +/// endpoint authorizes `import` itself and never asks for `insert`, so +/// requiring one here would hide tables the server would happily accept. pub(crate) async fn load_catalog( state: AppState, headers: &HeaderMap, transfer_action: &str, - row_action: &str, + row_action: Option<&str>, ) -> Result { let request = authenticated_request(headers, GetAuthorizationRequest {}) .map_err(|_| LoadError::Unauthenticated)?; @@ -58,7 +60,9 @@ pub(crate) async fn load_catalog( } let permits = |profile: &str, table: &str| { crate::authz::permits_table(&authorization, profile, table, transfer_action) - && crate::authz::permits_table(&authorization, profile, table, row_action) + && row_action.is_none_or(|action| { + crate::authz::permits_table(&authorization, profile, table, action) + }) }; let mut definitions = state.definitions; let tree = definitions diff --git a/web/src/pages/import_export/export/loader.rs b/web/src/pages/import_export/export/loader.rs index 88c01b63..c766c6b5 100644 --- a/web/src/pages/import_export/export/loader.rs +++ b/web/src/pages/import_export/export/loader.rs @@ -8,7 +8,7 @@ pub(crate) async fn load_page( state: AppState, headers: &HeaderMap, ) -> Result { - let catalog = load_catalog(state, headers, crate::authz::EXPORT, "read").await?; + let catalog = load_catalog(state, headers, crate::authz::EXPORT, Some("read")).await?; Ok(ExportPageState { nav: crate::ui::Nav::from_authorization(headers, "", &catalog.authorization), catalog, diff --git a/web/src/pages/import_export/import/loader.rs b/web/src/pages/import_export/import/loader.rs index c3e9ac63..38f6c365 100644 --- a/web/src/pages/import_export/import/loader.rs +++ b/web/src/pages/import_export/import/loader.rs @@ -13,7 +13,7 @@ pub(crate) async fn load_page( form: ImportForm, step: Step, ) -> Result { - let catalog = load_catalog(state, headers, crate::authz::IMPORT, "insert").await?; + let catalog = load_catalog(state, headers, crate::authz::IMPORT, None).await?; Ok(ImportPageState { nav: crate::ui::Nav::from_authorization(headers, "", &catalog.authorization), catalog,