diff --git a/adk-rust/crates/detmir-portal/src/main.rs b/adk-rust/crates/detmir-portal/src/main.rs index a79a34f..ac58323 100644 --- a/adk-rust/crates/detmir-portal/src/main.rs +++ b/adk-rust/crates/detmir-portal/src/main.rs @@ -24,6 +24,7 @@ use sha2::{Digest, Sha256}; use tiny_http::{Header, Method, Request, Response, Server, StatusCode}; mod executive_actions; +mod portal_roles; mod production; mod risk_narrative; mod workforce_kpi_explain; @@ -31,6 +32,7 @@ mod workforce_kpi_explain; use executive_actions::{ actions_from_center, build_action_center_from_report, filter_actions_for_role, }; +use portal_roles::PortalRole; use production::{ build_healthz, build_readyz, build_version, http_request_metadata, is_limited_api_route, log_http_request, mark_request_started, record_http_metric, record_ingestion_accepted, @@ -75,76 +77,6 @@ unsafe extern "C" { type SnapshotCache = Arc>>; -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] -#[serde(rename_all = "snake_case")] -enum PortalRole { - Executive, - Manager, - Security, - Forensics, - Admin, -} - -impl PortalRole { - fn parse(value: &str) -> Option { - match value.trim().to_ascii_lowercase().as_str() { - "executive" | "owner" | "rukovoditel" | "руководитель" => { - Some(Self::Executive) - } - "manager" | "workforce" | "руководитель_подразделения" => { - Some(Self::Manager) - } - "security" | "ib" | "soc" | "безопасность" => Some(Self::Security), - "forensics" | "investigation" | "расследования" => Some(Self::Forensics), - "admin" | "operations" | "operator" | "эксплуатация" => Some(Self::Admin), - _ => None, - } - } - - fn as_str(self) -> &'static str { - match self { - Self::Executive => "executive", - Self::Manager => "manager", - Self::Security => "security", - Self::Forensics => "forensics", - Self::Admin => "admin", - } - } - - fn label_ru(self) -> &'static str { - match self { - Self::Executive => "Руководитель", - Self::Manager => "Руководитель подразделения", - Self::Security => "Безопасность", - Self::Forensics => "Расследования", - Self::Admin => "Администратор", - } - } - - fn allowed_scopes(self) -> &'static [&'static str] { - match self { - Self::Executive => &["executive", "workforce"], - Self::Manager => &["executive", "workforce"], - Self::Security => &["security", "incidents", "ueba", "pfsense"], - Self::Forensics => &["forensics", "incidents", "ueba"], - Self::Admin => &[ - "executive", - "workforce", - "security", - "forensics", - "incidents", - "ueba", - "pfsense", - "admin", - ], - } - } - - fn can_access(self, scope: &str) -> bool { - self.allowed_scopes().contains(&scope) - } -} - #[derive(Clone, Debug)] struct CachedSnapshot { created: Instant, diff --git a/adk-rust/crates/detmir-portal/src/portal_roles.rs b/adk-rust/crates/detmir-portal/src/portal_roles.rs new file mode 100644 index 0000000..77dd105 --- /dev/null +++ b/adk-rust/crates/detmir-portal/src/portal_roles.rs @@ -0,0 +1,77 @@ +//! Portal role model and access-scope contract. +//! +//! CONTRACT: role aliases, serialized values and allowed scopes are part of +//! the portal API/security boundary. Keep changes explicit and covered by +//! existing role-gate tests in `main.rs`. + +use serde::Serialize; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub(crate) enum PortalRole { + Executive, + Manager, + Security, + Forensics, + Admin, +} + +impl PortalRole { + pub(crate) fn parse(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "executive" | "owner" | "rukovoditel" | "руководитель" => { + Some(Self::Executive) + } + "manager" | "workforce" | "руководитель_подразделения" => { + Some(Self::Manager) + } + "security" | "ib" | "soc" | "безопасность" => Some(Self::Security), + "forensics" | "investigation" | "расследования" => Some(Self::Forensics), + "admin" | "operations" | "operator" | "эксплуатация" => Some(Self::Admin), + _ => None, + } + } + + pub(crate) fn as_str(self) -> &'static str { + match self { + Self::Executive => "executive", + Self::Manager => "manager", + Self::Security => "security", + Self::Forensics => "forensics", + Self::Admin => "admin", + } + } + + pub(crate) fn label_ru(self) -> &'static str { + match self { + Self::Executive => "Руководитель", + Self::Manager => "Руководитель подразделения", + Self::Security => "Безопасность", + Self::Forensics => "Расследования", + Self::Admin => "Администратор", + } + } + + pub(crate) fn allowed_scopes(self) -> &'static [&'static str] { + match self { + Self::Executive => &["executive", "workforce"], + Self::Manager => &["executive", "workforce"], + Self::Security => &["security", "incidents", "ueba", "pfsense"], + Self::Forensics => &["forensics", "incidents", "ueba"], + Self::Admin => &[ + "executive", + "workforce", + "security", + "forensics", + "incidents", + "ueba", + "pfsense", + "admin", + ], + } + } + + pub(crate) fn can_access(self, scope: &str) -> bool { + self.allowed_scopes().contains(&scope) + } +}