fix(ui): проверять права каждого источника дашборда
This commit is contained in:
1 parent
1999e8160e
commit
587dd94f82
7 files changed
+59
-5
No files matched your search
+1
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
## Актуальная точка продолжения — 2026-10-03
|
||||
|
||||
Этап 11Б в работе; последний проверенный блок 11Б.4.4.3.6; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.4.3.7: применить права backend к каждому запросу dashboard reader и проверить ограниченный JWT; затем адаптивность/pickers, Releases/probe scheduling и приёмка 11Б/12. Processing recovery требует отдельного сценария остановки отправителя.
|
||||
Этап 11Б в работе; последний проверенный блок 11Б.4.4.3.7; System DB принят локально с внешними ограничениями. Следующий блок 11Б.4.4.3.8: убрать timeout/partial/error→0 в backend sidebar counts через полные каталоги; затем адаптивность/pickers, Releases/probe scheduling и приёмка 11Б/12. Processing recovery требует отдельного сценария остановки отправителя.
|
||||
|
||||
Реальные pg_dump/pg_restore, PG rollback и остановка UDP worker требуют внешней приёмки. Датированные продолжения ниже — история; точный следующий шаг также указан в начале [журнала](ux-ui-progress.md).
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | Завершён в границах локальной приёмки 2026-10-01; блоки 10.1–10.71, внешние ограничения в 10.71 |
|
||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Завершён в границах локальной приёмки 2026-10-01; 11А.1–11А.33, внешние ограничения в 11А.33 |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.4.4.3.6; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | В работе: 11Б.1–11Б.4.4.3.7; scheduler и GeoIP приняты локально; NetFlow принят локально; Evolution принят локально; далее общие Settings |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
|
||||
|
||||
|
||||
## Блок 11Б.4.4.3.7 — права каждого источника дашборда (2026-10-03)
|
||||
|
||||
- Все запросы useDashboardLive проходят ensureAuthConfig и per-path/method gate до запроса и публикации, включая standalone WireGuard/events. Dashboard:read не выдаёт servers/uptime/filters/BGP/network/traffic rights; fallback diagnostic POST требует servers:write как на backend. Unknown route не получает разрешение по умолчанию. Отозванные источники очищаются перед обновлением.
|
||||
- Mapper сверяется unit-тестом с реальным backend permissionForRequest для 13 используемых API-путей. Legacy VXLAN GET на backend использует dashboard:read; frontend сохраняет эту политику, не меняет серверные права или сетевые операции.
|
||||
- 18 browser scenarios dashboard/limited JWT/sidebar/alerts targets, mapper unit, typecheck/lint прошли. Дополнительная общая regression из 11Б.4.4.3.6 завершилась: 107/107 dashboard/GRE/uptime/VXLAN/WireGuard scenarios прошли за 3.4 минуты. Это профильная регрессия общей оболочки, не полная приёмка этапа 12.
|
||||
- Сам dashboard ещё использует некоторые legacy DTO/нулевые fallback для неизвестных полей; текущий блок исправляет права, не заявляет новую приёмку достоверности всех upstream данных. Реальный RouterOS/PG не проверялся.
|
||||
- Следующий точный шаг 11Б.4.4.3.8: убрать timeout/partial/error→0 в backend sidebar counts через полные каталоги; затем адаптивность/pickers, Releases/probe scheduling и приёмка 11Б/12. Processing recovery требует отдельного сценария остановки отправителя.
|
||||
|
||||
## Блок 11Б.4.4.3.6 — права и достоверность общей сводки (2026-10-03)
|
||||
|
||||
- AppSidebar ждёт ensureAuthConfig, проверяет dashboard:read для counts и filters:read для GRE перед запросом и публикацией. Читатели независимы, bounded/abort/revision/source tag не дают старому ответу заменить новый. Badge скрывается при отсутствии права раздела, неизвестное optional поле не превращается в 0, partial GRE сохраняет последний complete count.
|
||||
|
||||
@@ -304,3 +304,7 @@ UI manual unknown retry принят локально: explicit duplicate risk,
|
||||
### Продолжение 2026-10-03 — 11Б.4.4.3.6
|
||||
|
||||
Общие Sidebar/SystemMonitor readers проверяют auth/права, unknown counts не становятся нулями, health независим от summary; API unit/browser9/typecheck/lint прошли. Дополнительная links/dashboard регрессия107 ещё выполняется. Backend count helpers с catch→0 требуют отдельного исправления. Следующий блок 11Б.4.4.3.7: применить права backend к каждому запросу dashboard reader и проверить ограниченный JWT; затем адаптивность/pickers, Releases/probe scheduling и приёмка 11Б/12. Processing recovery требует отдельного сценария остановки отправителя.
|
||||
|
||||
### Продолжение 2026-10-03 — 11Б.4.4.3.7
|
||||
|
||||
Dashboard per-request permissions принят локально: mapper parity13paths, limited JWT2, browser18/typecheck/lint прошли. Предыдущая regression107 завершилась успешно. Legacy dashboard DTO/fallback не переприняты. Следующий блок 11Б.4.4.3.8: убрать timeout/partial/error→0 в backend sidebar counts через полные каталоги; затем адаптивность/pickers, Releases/probe scheduling и приёмка 11Б/12. Processing recovery требует отдельного сценария остановки отправителя.
|
||||
@@ -31,12 +31,21 @@ import type { FiltersRulesetRow, RouteOptimizerSpeedProbe } from "@/lib/route-op
|
||||
|
||||
import { updateDashboardSource, type DashboardSource, type DashboardSources } from "@/lib/dashboard-source-state"
|
||||
|
||||
import {can,ensureAuthConfig} from "@/lib/auth"
|
||||
import {dashboardRequestPermission} from "@/lib/dashboard-permissions"
|
||||
async function dashboardRequest<T>(path:string,work:()=>Promise<T>,method="GET"):Promise<T>{
|
||||
await ensureAuthConfig();const permission=dashboardRequestPermission(method,path)
|
||||
if(!can(permission))throw Error("Нет доступа к источнику: "+permission)
|
||||
const result=await work();if(!can(permission))throw Error("Доступ к источнику отозван: "+permission)
|
||||
return result
|
||||
}
|
||||
|
||||
const MOCK_DASH_STARS_LS = "mm:dashboard-probe-ids"
|
||||
const UPTIME_PROBES_CHANGED = "mm:uptime-probes-changed"
|
||||
|
||||
function makeApiFetch(backendUrl: string) {
|
||||
return async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
return requestJson<T>(backendUrl, path, init)
|
||||
return dashboardRequest(path,()=>requestJson<T>(backendUrl,path,init),init?.method??"GET")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,6 +282,13 @@ export function useDashboardLive() {
|
||||
setLoading(true)
|
||||
setInternetPathLoading(true)
|
||||
try {
|
||||
await ensureAuthConfig();if(!current())return
|
||||
if(!can("mm:servers:read"))setLiveServers(null)
|
||||
if(!can("mm:uptime:read"))setLiveProbes(null)
|
||||
if(!can("mm:bgp:read"))setBgp(null)
|
||||
if(!can("mm:traffic:read"))setTrafficSeries(null)
|
||||
if(!can("mm:network:read"))setInternetPath(null)
|
||||
setOverlayItems(previous=>previous?.filter(item=>can(item.kind==="gre"?"mm:filters:read":item.kind==="wg"?"mm:network:read":"mm:dashboard:read"))??null)
|
||||
const [overviewRes, serversRes, fr, br, ipRes, greRes, wgRes, trafficRes, vxRes] = await Promise.allSettled([
|
||||
apiFetch<{ probes: PingProbe[] }>("/api/uptime/overview?range=1h"),
|
||||
apiFetch<BackendServerRow[]>("/api/servers"),
|
||||
@@ -280,7 +296,7 @@ export function useDashboardLive() {
|
||||
apiFetch<Array<{ state?: string; prefixesRx?: number }>>("/api/bgp/sessions"),
|
||||
apiFetch<{ snapshot: InternetPathSnapshotPayload | null }>("/api/internet-path/latest"),
|
||||
apiFetch<{ tunnels?: ApiGreTunnelRow[] }>("/api/filters/gre-tunnels"),
|
||||
listWireGuard(backendUrl),
|
||||
dashboardRequest("/api/wireguard",()=>listWireGuard(backendUrl)),
|
||||
apiFetch<{ servers?: TrafficServerRow[] }>("/api/traffic/servers?range=1h"),
|
||||
apiFetch<{ tunnels?: Array<{ id: string; serverId: string; name: string; status: OverlayItem["status"] }> }>("/api/vxlan"),
|
||||
])
|
||||
@@ -458,7 +474,7 @@ export function useDashboardLive() {
|
||||
const current = () => request === eventsRequest.current
|
||||
setEventsLoading(true)
|
||||
try {
|
||||
const rows = await listEvents(backendUrl, { limit: 6 })
|
||||
const rows = await dashboardRequest("/api/events",()=>listEvents(backendUrl, { limit: 6 }))
|
||||
if (!current()) return
|
||||
setRecentEvents(rows)
|
||||
recordSource("events", null)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import assert from "node:assert/strict"
|
||||
import {test} from "node:test"
|
||||
import {dashboardRequestPermission} from "./dashboard-permissions"
|
||||
import {permissionForRequest} from "../backend/src/lib/permissions"
|
||||
test("dashboard reader gates match real backend permissions including fallback diagnostic POST",()=>{
|
||||
for(const [method,path] of [["GET","/api/uptime/overview?range=1h"],["GET","/api/servers"],["GET","/api/filters/rules"],["GET","/api/bgp/sessions"],["GET","/api/internet-path/latest"],["GET","/api/filters/gre-tunnels"],["GET","/api/wireguard"],["GET","/api/traffic/servers?range=1h"],["GET","/api/vxlan"],["GET","/api/events?limit=6"],["GET","/api/servers/1/wan-runtime"],["POST","/api/servers/1/probes/run"],["GET","/api/uptime/speed-probes"]])assert.equal(dashboardRequestPermission(method,path),permissionForRequest(method,path))
|
||||
assert.throws(()=>dashboardRequestPermission("GET","/api/unmapped"));assert.throws(()=>dashboardRequestPermission("POST","/api/events"))
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
/** Limited to API paths consumed by useDashboardLive; matches backend request gates. */
|
||||
export function dashboardRequestPermission(method:string,path:string):string{
|
||||
const pathname=path.split("?")[0],write=method.toUpperCase()!=="GET"
|
||||
if(pathname.startsWith("/api/servers"))return write?"mm:servers:write":"mm:servers:read"
|
||||
if(pathname.startsWith("/api/uptime"))return write?"mm:uptime:write":"mm:uptime:read"
|
||||
if(pathname.startsWith("/api/filters"))return write?"mm:filters:write":"mm:filters:read"
|
||||
if(pathname.startsWith("/api/bgp"))return write?"mm:bgp:write":"mm:bgp:read"
|
||||
if(pathname.startsWith("/api/traffic"))return write?"mm:traffic:write":"mm:traffic:read"
|
||||
if(pathname.startsWith("/api/internet-path")||pathname.startsWith("/api/wireguard"))return write?"mm:network:write":"mm:network:read"
|
||||
if(!write&&(pathname==="/api/vxlan"||pathname.startsWith("/api/events")))return "mm:dashboard:read"
|
||||
throw Error("Неизвестный источник дашборда")
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import {test,expect} from "@playwright/test"
|
||||
for(const bgp of [false,true])test("Dashboard limited JWT requests only authorized data sources/"+bgp,async({page})=>{
|
||||
const permissions=["mm:dashboard:read",...(bgp?["mm:bgp:read"]:[])],token="header."+Buffer.from(JSON.stringify({sub:"fixture",apps:["mm"],permissions,exp:Math.floor(Date.now()/1000)+3600})).toString("base64url")+".fixture"
|
||||
await page.addInitScript(value=>localStorage.setItem("mmapp_token",value),token);const calls:string[]=[]
|
||||
await page.route("**/health",r=>r.fulfill({json:{status:"ok"}}));await page.route("**/api/**",r=>{const path=new URL(r.request().url()).pathname;if(path==="/api/auth/config")return r.fulfill({json:{required:true}});calls.push(path);return r.fulfill({json:path==="/api/vxlan"?{tunnels:[]}:path==="/api/events"?{events:[]}:path==="/api/bgp/sessions"?[{state:"established",prefixesRx:321}]:{}})});await page.goto("/dashboard");const refresh=page.getByRole("button",{name:"Обновить данные",exact:true});await expect(refresh).toBeEnabled();await expect(page.locator('[data-source="servers"]')).toContainText("Нет доступа");await refresh.click();await expect(refresh).toBeEnabled();for(const path of ["/api/servers","/api/uptime/overview","/api/filters/rules","/api/filters/gre-tunnels","/api/internet-path/latest","/api/wireguard","/api/traffic/servers"])expect(calls).not.toContain(path);expect(calls.includes("/api/bgp/sessions")).toBe(bgp);const tile=page.getByRole("link").filter({hasText:"Префиксы BGP"});await expect(tile).toContainText(bgp?"321":"—");expect(calls).toContain("/api/vxlan")
|
||||
})
|
||||
Reference in new issue
Block a user