web synchronized with the new changes

This commit is contained in:
Priec
2026-08-09 16:30:27 +02:00
parent e66b025188
commit fd551ef7fb
44 changed files with 1215 additions and 88 deletions

View File

@@ -13,7 +13,7 @@ use tonic::transport::Channel;
use crate::{
AppState,
auth::GetAuthorizationRequest,
auth::{GetAuthorizationRequest, ListGrantableObjectsRequest, ListRolePermissionsRequest, ListRolesRequest},
definitions::{
common::Empty,
table_definition::{
@@ -27,7 +27,7 @@ use crate::{
use super::state::{
DetailColumn, LoadError, PageInputs, RenameEntry, ScriptView, TableDefinitionPageState,
TableDetailView, TableSummary,
TableDetailView, TablePermissionAction, TableRolePermissions, TableSummary,
};
/// Reads the column-type vocabulary on its own.
@@ -67,7 +67,7 @@ pub(crate) async fn load_page(
_ => LoadError::Backend(error.message().to_string()),
})?
.into_inner();
if authorization.role != "admin" {
if !crate::authz::can_manage(&authorization, crate::authz::STRUCT_TABLE) {
return Err(LoadError::Forbidden);
}
@@ -220,8 +220,83 @@ pub(crate) async fn load_page(
false => Vec::new(),
};
let mut permission_object = String::new();
let mut role_permissions = Vec::new();
if inputs.selection.has_table()
&& crate::authz::can_manage(&authorization, crate::authz::STRUCT_ROLE)
{
let expected_object = crate::authz::table_object(
&inputs.selection.profile,
&inputs.selection.table,
);
let roles = auth
.list_roles(
authenticated_request(headers, ListRolesRequest {})
.map_err(|_| LoadError::Unauthenticated)?,
)
.await
.map_err(|error| LoadError::Backend(error.message().to_string()))?
.into_inner()
.roles;
for role in roles.into_iter().filter(|role| role.kind == "data") {
let grantable = auth
.list_grantable_objects(
authenticated_request(
headers,
ListGrantableObjectsRequest {
target_role: role.name.clone(),
},
)
.map_err(|_| LoadError::Unauthenticated)?,
)
.await
.map_err(|error| LoadError::Backend(error.message().to_string()))?
.into_inner()
.objects
.into_iter()
.find(|object| object.object == expected_object);
let Some(grantable) = grantable else {
continue;
};
permission_object = expected_object.clone();
let permissions = auth
.list_role_permissions(
authenticated_request(
headers,
ListRolePermissionsRequest {
role: role.name.clone(),
},
)
.map_err(|_| LoadError::Unauthenticated)?,
)
.await
.map_err(|error| LoadError::Backend(error.message().to_string()))?
.into_inner();
role_permissions.push(TableRolePermissions {
role: role.name,
actions: grantable
.allowed_actions
.into_iter()
.map(|action| TablePermissionAction {
direct: crate::authz::is_direct_permission(
&permissions.permissions,
&expected_object,
&action,
),
effective: crate::authz::permissions_permit(
&permissions.effective_permissions,
&expected_object,
&action,
),
action,
})
.collect(),
});
}
}
Ok(TableDefinitionPageState {
nav: crate::ui::Nav::new(headers, "admin").with_role(authorization.role),
nav: crate::ui::Nav::new(headers, "admin").with_authorization(&authorization),
profiles,
tables,
detail,
@@ -235,5 +310,7 @@ pub(crate) async fn load_page(
error: inputs.error,
sql: inputs.sql,
generated: inputs.generated,
permission_object,
role_permissions,
})
}

View File

@@ -462,7 +462,7 @@ fn load_error_response(error: LoadError) -> Response {
LoadError::Forbidden => (
StatusCode::FORBIDDEN,
Html(ui::render_load_error(
"Administrator access is required.",
"Table-management permission is required.",
)),
)
.into_response(),

View File

@@ -255,6 +255,19 @@ pub(crate) struct TableDefinitionPageState {
pub error: Option<String>,
pub sql: Option<String>,
pub generated: Vec<GeneratedTableView>,
pub permission_object: String,
pub role_permissions: Vec<TableRolePermissions>,
}
pub(crate) struct TableRolePermissions {
pub role: String,
pub actions: Vec<TablePermissionAction>,
}
pub(crate) struct TablePermissionAction {
pub action: String,
pub direct: bool,
pub effective: bool,
}
impl TableDefinitionPageState {

View File

@@ -99,7 +99,7 @@ mod tests {
use crate::{
pages::admin::table_definition::state::{
CopyForm, DetailColumn, InvoiceTemplateForm, RenameForm, Selection, TableDetailView,
TableSummary,
TablePermissionAction, TableRolePermissions, TableSummary,
},
schema::ColumnDraft,
};
@@ -149,6 +149,8 @@ mod tests {
error: None,
sql: None,
generated: Vec::new(),
permission_object: String::new(),
role_permissions: Vec::new(),
}
}
@@ -296,6 +298,26 @@ mod tests {
assert!(html.contains("1 column added"));
}
#[test]
fn selected_table_exposes_its_data_permission_actions() {
let mut page = page();
page.permission_object = "data:billing/invoice".to_string();
page.role_permissions = vec![TableRolePermissions {
role: "bookkeeper".to_string(),
actions: vec![TablePermissionAction {
action: "read".to_string(),
direct: false,
effective: false,
}],
}];
let html = render_workspace(&page);
assert!(!html.contains("Template error"), "{html}");
assert!(html.contains("Data permissions for"));
assert!(html.contains("data:billing/invoice"));
assert!(html.contains("Grant read"));
}
#[test]
fn a_load_failure_answers_with_the_dialog() {
let html = render_load_error("The backend is unreachable.");