From ff044d65f1e42ba6361e5c856a0afba571e40475 Mon Sep 17 00:00:00 2001 From: Priec Date: Sun, 9 Aug 2026 18:49:54 +0200 Subject: [PATCH] web permission system updated --- server | 2 +- web/CHANGELOG.md | 8 ++-- web/src/pages/admin/permissions/loader.rs | 2 + web/src/pages/admin/permissions/logic.rs | 47 ++++++++++--------- web/src/pages/admin/permissions/state.rs | 3 ++ web/src/pages/admin/permissions/ui.rs | 1 + web/src/pages/login/logic.rs | 1 - web/src/pages/login/state.rs | 2 - web/src/pages/login/ui.rs | 3 -- .../pages/admin/permissions/permissions.html | 2 + .../admin/table_definition/workspace.html | 2 +- web/templates/pages/login/login.html | 1 - 12 files changed, 40 insertions(+), 34 deletions(-) diff --git a/server b/server index b88981db..068cfbdb 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit b88981db0587ff651c7b4d43b778eeff6dfc06e1 +Subproject commit 068cfbdba058645ae7035abc586a31a0f2dbf493 diff --git a/web/CHANGELOG.md b/web/CHANGELOG.md index fbd08783..60e7ee85 100644 --- a/web/CHANGELOG.md +++ b/web/CHANGELOG.md @@ -44,10 +44,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **Analytics and import/export use the caller's visible data** — profile-tree discovery is authenticated, export is limited to readable tables, and import is limited to tables for which the snapshot carries `insert`. -- **Permission mutations retire the browser session** — the current backend - invalidates every existing token after a policy change. After a successful - role, assignment, grant or revoke operation the web clears that stale cookie - and explains why the administrator must sign in again. +- **Permission mutations keep the browser session** — grants, role changes and + assignments now reload the live authorization state without replacing the + identity-only JWT. The permissions workspace reloads in place and shows the + updated effective policy. - **The column-type picker is the server's list** — it no longer carries its own. Types the web crate never offered are now reachable: `numeric` and the `ACCOUNTING_TRANSFER` compound column. Server-generated companion types diff --git a/web/src/pages/admin/permissions/loader.rs b/web/src/pages/admin/permissions/loader.rs index 387188e8..fee86bc2 100644 --- a/web/src/pages/admin/permissions/loader.rs +++ b/web/src/pages/admin/permissions/loader.rs @@ -16,6 +16,7 @@ pub(crate) async fn load_page( headers: &HeaderMap, selection: Selection, ) -> Result { + let updated = selection.updated; let mut auth = state.auth; let authorization = auth .get_authorization( @@ -120,6 +121,7 @@ pub(crate) async fn load_page( grantable_objects, can_manage_roles, can_manage_users, + updated, }) } diff --git a/web/src/pages/admin/permissions/logic.rs b/web/src/pages/admin/permissions/logic.rs index 4618f7ea..f372a42e 100644 --- a/web/src/pages/admin/permissions/logic.rs +++ b/web/src/pages/admin/permissions/logic.rs @@ -36,15 +36,17 @@ pub(crate) async fn add_role( headers: HeaderMap, Form(form): Form, ) -> Response { + let role = form.name.trim().to_string(); + let destination = format!("/admin/permissions?role={role}&updated=true"); let request_headers = headers.clone(); - mutate(&headers, async move { + mutate(&headers, &destination, async move { let mut auth = state.auth; auth.add_role(authenticated_request(&request_headers, AddRoleRequest { - name: form.name.trim().to_string(), + name: role, parent: form.parent.trim().to_string(), }).map_err(|_| "Missing session".to_string())?) .await.map_err(|error| error.message().to_string())?; - Ok("Role created. Sign in again to continue.") + Ok(()) }).await } @@ -53,14 +55,15 @@ pub(crate) async fn remove_role( headers: HeaderMap, Form(form): Form, ) -> Response { + let destination = "/admin/permissions?updated=true"; let request_headers = headers.clone(); - mutate(&headers, async move { + mutate(&headers, destination, async move { let mut auth = state.auth; auth.remove_role(authenticated_request(&request_headers, RemoveRoleRequest { name: form.role, }).map_err(|_| "Missing session".to_string())?) .await.map_err(|error| error.message().to_string())?; - Ok("Role removed. Sign in again to continue.") + Ok(()) }).await } @@ -69,8 +72,9 @@ pub(crate) async fn grant( headers: HeaderMap, Form(form): Form, ) -> Response { + let destination = format!("/admin/permissions?role={}&updated=true", form.role); let request_headers = headers.clone(); - mutate(&headers, async move { + mutate(&headers, &destination, async move { let mut auth = state.auth; auth.grant_permission(authenticated_request(&request_headers, GrantPermissionRequest { role: form.role, @@ -78,7 +82,7 @@ pub(crate) async fn grant( action: form.action, }).map_err(|_| "Missing session".to_string())?) .await.map_err(|error| error.message().to_string())?; - Ok("Permission granted. Sign in again to continue.") + Ok(()) }).await } @@ -87,8 +91,9 @@ pub(crate) async fn revoke( headers: HeaderMap, Form(form): Form, ) -> Response { + let destination = format!("/admin/permissions?role={}&updated=true", form.role); let request_headers = headers.clone(); - mutate(&headers, async move { + mutate(&headers, &destination, async move { let mut auth = state.auth; auth.revoke_permission(authenticated_request(&request_headers, RevokePermissionRequest { role: form.role, @@ -96,7 +101,7 @@ pub(crate) async fn revoke( action: form.action, }).map_err(|_| "Missing session".to_string())?) .await.map_err(|error| error.message().to_string())?; - Ok("Permission revoked. Sign in again to continue.") + Ok(()) }).await } @@ -105,27 +110,28 @@ pub(crate) async fn assign_user_role( headers: HeaderMap, Form(form): Form, ) -> Response { + let destination = "/admin/permissions?updated=true"; let request_headers = headers.clone(); - mutate(&headers, async move { + mutate(&headers, destination, async move { let mut auth = state.auth; auth.assign_user_role(authenticated_request(&request_headers, AssignUserRoleRequest { username: form.username, role: form.role, }).map_err(|_| "Missing session".to_string())?) .await.map_err(|error| error.message().to_string())?; - Ok("User role changed. Sign in again to continue.") + Ok(()) }).await } -async fn mutate(headers: &HeaderMap, operation: F) -> Response +async fn mutate(headers: &HeaderMap, destination: &str, operation: F) -> Response where - F: std::future::Future>, + F: std::future::Future>, { if let Some(rejection) = reject_cross_site(headers) { return rejection; } match operation.await { - Ok(_) => stale_session_response(), + Ok(()) => success_redirect(destination), Err(message) => ( StatusCode::UNPROCESSABLE_ENTITY, Html(ui::render_mutation_error(&message)), @@ -133,19 +139,18 @@ where } } -fn stale_session_response() -> Response { +fn success_redirect(destination: &str) -> Response { let mut response = StatusCode::SEE_OTHER.into_response(); - response.headers_mut().insert( - header::SET_COOKIE, - HeaderValue::from_static("analytics_token=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0"), - ); + let Ok(destination) = HeaderValue::try_from(destination) else { + return (StatusCode::INTERNAL_SERVER_ERROR, "Invalid redirect").into_response(); + }; response.headers_mut().insert( header::LOCATION, - HeaderValue::from_static("/login?permissions_changed=1"), + destination.clone(), ); response.headers_mut().insert( "hx-redirect", - HeaderValue::from_static("/login?permissions_changed=1"), + destination, ); response } diff --git a/web/src/pages/admin/permissions/state.rs b/web/src/pages/admin/permissions/state.rs index aa6f6823..fd95acb8 100644 --- a/web/src/pages/admin/permissions/state.rs +++ b/web/src/pages/admin/permissions/state.rs @@ -4,6 +4,8 @@ use crate::auth::{GrantableObject, Permission, Role, UserSummary}; pub(crate) struct Selection { #[serde(default)] pub role: String, + #[serde(default)] + pub updated: bool, } #[derive(Clone, Debug, Default, serde::Deserialize)] @@ -41,6 +43,7 @@ pub(crate) struct PermissionPageState { pub grantable_objects: Vec, pub can_manage_roles: bool, pub can_manage_users: bool, + pub updated: bool, } impl PermissionPageState { diff --git a/web/src/pages/admin/permissions/ui.rs b/web/src/pages/admin/permissions/ui.rs index 582fe961..7d2de5e5 100644 --- a/web/src/pages/admin/permissions/ui.rs +++ b/web/src/pages/admin/permissions/ui.rs @@ -85,6 +85,7 @@ mod tests { }], can_manage_roles: true, can_manage_users: true, + updated: false, }; let html = render_page(&page); diff --git a/web/src/pages/login/logic.rs b/web/src/pages/login/logic.rs index 87b00c17..5a548e46 100644 --- a/web/src/pages/login/logic.rs +++ b/web/src/pages/login/logic.rs @@ -16,7 +16,6 @@ pub(crate) async fn login_page( ) -> Html { Html(ui::render_page( Nav::new(&headers, "login"), - query.permissions_changed, query.initial_password_set, )) } diff --git a/web/src/pages/login/state.rs b/web/src/pages/login/state.rs index 955e2c94..e14917cd 100644 --- a/web/src/pages/login/state.rs +++ b/web/src/pages/login/state.rs @@ -7,8 +7,6 @@ pub(crate) struct LoginInput { #[derive(Default, serde::Deserialize)] pub(crate) struct LoginQuery { - #[serde(default)] - pub permissions_changed: bool, #[serde(default)] pub initial_password_set: bool, } diff --git a/web/src/pages/login/ui.rs b/web/src/pages/login/ui.rs index ad9c0d03..05563845 100644 --- a/web/src/pages/login/ui.rs +++ b/web/src/pages/login/ui.rs @@ -7,18 +7,15 @@ use crate::ui::{Alert, Nav, render}; #[template(path = "pages/login/login.html")] struct LoginPage { nav: Nav, - permissions_changed: bool, initial_password_set: bool, } pub(crate) fn render_page( nav: Nav, - permissions_changed: bool, initial_password_set: bool, ) -> String { render(&LoginPage { nav, - permissions_changed, initial_password_set, }) } diff --git a/web/templates/pages/admin/permissions/permissions.html b/web/templates/pages/admin/permissions/permissions.html index 1d0c6a86..a0e46940 100644 --- a/web/templates/pages/admin/permissions/permissions.html +++ b/web/templates/pages/admin/permissions/permissions.html @@ -13,6 +13,8 @@ + {% if page.updated %}

Authorization updated. The current session remains valid and all subsequent requests use the new policy.

{% endif %} + {% if page.can_manage_roles %}

Data roles

diff --git a/web/templates/pages/admin/table_definition/workspace.html b/web/templates/pages/admin/table_definition/workspace.html index 8a711b8c..4c2f8fa1 100644 --- a/web/templates/pages/admin/table_definition/workspace.html +++ b/web/templates/pages/admin/table_definition/workspace.html @@ -154,7 +154,7 @@ {% if !page.permission_object.is_empty() %}

Data permissions for {{ page.selection.table }}

-

These grants cover this table family. A successful change retires the current session, so you will be asked to sign in again.

+

These grants cover this table family and take effect on subsequent requests without replacing the current session.

{% for role in page.role_permissions %} diff --git a/web/templates/pages/login/login.html b/web/templates/pages/login/login.html index 0506fe2a..6501b4b1 100644 --- a/web/templates/pages/login/login.html +++ b/web/templates/pages/login/login.html @@ -8,7 +8,6 @@

Sign in

- {% if permissions_changed %}

Authorization changed successfully. Sign in again because the server retired the previous session.

{% endif %} {% if initial_password_set %}

The initial password was set. You can sign in now.

{% endif %}
RoleActions