From 643d5d2d69c57e8ee206fd17eedeec9d31ddef95 Mon Sep 17 00:00:00 2001 From: igor04091968 Date: Mon, 15 Jun 2026 00:00:04 +0300 Subject: [PATCH] refactor(portal): move role access helpers into module --- .../crates/detmir-portal/src/role_access.rs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 adk-rust/crates/detmir-portal/src/role_access.rs diff --git a/adk-rust/crates/detmir-portal/src/role_access.rs b/adk-rust/crates/detmir-portal/src/role_access.rs new file mode 100644 index 0000000..dfaa166 --- /dev/null +++ b/adk-rust/crates/detmir-portal/src/role_access.rs @@ -0,0 +1,52 @@ +//! Portal role extraction and access-denial helpers. +//! +//! CONTRACT: role aliases, role envelope fields and forbidden response shape +//! are part of the portal security boundary. Keep changes explicit and covered +//! by role-gate tests. + +use anyhow::Result; +use serde_json::{Value, json}; +use tiny_http::{Request, StatusCode}; + +use crate::path_query::query_param; +use crate::portal_roles::PortalRole; +use crate::respond_json_status; + +pub(crate) fn portal_role_from_request(request: &Request, url: &str) -> PortalRole { + query_param(url, "role") + .as_deref() + .and_then(PortalRole::parse) + .or_else(|| { + request + .headers() + .iter() + .find(|header| header.field.equiv("X-AWatch-Role")) + .and_then(|header| PortalRole::parse(header.value.as_str())) + }) + .unwrap_or(PortalRole::Executive) +} + +pub(crate) fn role_envelope(role: PortalRole, scope: &str) -> Value { + json!({ + "role": role.as_str(), + "role_label": role.label_ru(), + "scope": scope, + "allowed_scopes": role.allowed_scopes(), + "server_enforced": true, + }) +} + +pub(crate) fn respond_forbidden(request: Request, role: PortalRole, scope: &str) -> Result<()> { + respond_json_status( + request, + StatusCode(403), + &json!({ + "ok": false, + "error": "forbidden", + "message": format!("Роль {} не имеет доступа к контуру {scope}", role.label_ru()), + "role": role.as_str(), + "scope": scope, + "server_enforced": true, + }), + ) +}