Compare commits

..
Author SHA1 Message Date
igor04091968 fa1ddf64b4 refactor(portal): wire role access module 2026-06-15 00:03:44 +03:00
igor04091968 643d5d2d69 refactor(portal): move role access helpers into module 2026-06-15 00:00:04 +03:00
IgorRachkovandGitHub b629879958 Merge pull request #32 from igor04091968/refactor/portal-readiness-api
refactor(portal): move readiness API helpers into module
2026-06-14 23:56:40 +03:00
2 changed files with 54 additions and 39 deletions
+2 -39
View File
@@ -31,6 +31,7 @@ mod portal_roles;
mod production;
mod readiness_api;
mod risk_narrative;
mod role_access;
mod workforce_kpi_explain;
use api_contracts::api_contract_summary;
@@ -52,6 +53,7 @@ use readiness_api::{readiness_bundle, readiness_latest, readiness_verify};
use risk_narrative::{
RiskNarrativeInputs, RiskNarrativeQuery, build_risk_narrative, build_risk_narrative_from_report,
};
use role_access::{portal_role_from_request, respond_forbidden, role_envelope};
use workforce_kpi_explain::{KpiExplainQuery, build_workforce_kpi_explain};
const INDEX_HTML: &str = include_str!("static/index.html");
@@ -1663,45 +1665,6 @@ fn handle_evidence_only_request(request: Request, args: &Cli) -> Result<()> {
)
}
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)
}
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,
})
}
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,
}),
)
}
fn cached_snapshot(args: &Cli, cache: &SnapshotCache) -> Snapshot {
let mut guard = cache.lock().expect("snapshot cache mutex poisoned");
if let Some(cached) = guard.as_ref() {
@@ -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,
}),
)
}