fix(auth): закрыть обход JWT через закодированные URL

This commit is contained in:
Denozordec committed 2026-10-13 21:23:51 +07:00
1 parent 727e35f985
commit 7d86af7e8b
19 files changed
+1656 -23

No files matched your search

+1 -1
View File
@@ -17,7 +17,7 @@
"facts:rebuild": "tsx src/scripts/rebuild-flow-facts.ts",
"test:probes": "tsx src/routes/probes.test.ts && tsx src/routes/probes.cancel.test.ts && tsx src/services/ping-measurement.test.ts && tsx src/services/uptime-probe-observations.test.ts && tsx src/services/ping-measurement-sql.test.ts",
"test:exec": "tsx src/services/mikrotik-logs.test.ts && tsx src/routes/exec.test.ts && tsx src/plugins/auth.exec.test.ts",
"test:auth": "tsx src/lib/permissions.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts",
"test:auth": "tsx src/plugins/auth-path.test.ts && tsx src/lib/permissions.test.ts && tsx src/plugins/auth.smoke.test.ts && tsx src/plugins/auth.ospf-optimizer.test.ts",
"test:wireguard": "npx tsx src/services/wireguard-config.test.ts && npx tsx src/services/wireguard-ipv6.test.ts && npx tsx --test src/services/wireguard-import.test.ts src/services/wireguard-import-apply.test.ts src/services/wireguard-import-revision.test.ts src/services/wireguard-delete.test.ts && npx tsx src/routes/wireguard-import.test.ts",
"test:traffic-rate": "tsx src/services/traffic-rate.test.ts && tsx src/services/traffic-observation.test.ts && tsx src/services/traffic-freshness.test.ts && tsx src/services/traffic-live.test.ts && tsx src/services/traffic-observation-sql.test.ts && tsx src/services/traffic-history-sql.test.ts && tsx src/services/traffic-live-sql.test.ts && tsx src/services/traffic-volume-sql.test.ts && tsx src/services/traffic-binding-status.test.ts && tsx src/services/traffic-status-sql.test.ts && tsx src/services/traffic-gap.test.ts && tsx src/services/traffic-identity.test.ts && tsx src/services/traffic-membership.test.ts && tsx src/services/traffic-aggregate-peak.test.ts && tsx src/services/traffic-users-cache.test.ts && tsx src/services/traffic-collection-lock.test.ts && tsx src/services/traffic-concurrency-sql.test.ts",
"test:traffic-flow": "tsx src/routes/traffic-flow-query.test.ts && tsx src/services/traffic-flow-month.test.ts && tsx src/services/traffic-flow-parse.test.ts && tsx src/services/traffic-flow-ipv6-pipeline.test.ts && tsx src/services/traffic-flow-map-exporter.test.ts && tsx src/services/traffic-flow-ifaces.test.ts && tsx src/services/traffic-flow-ifindex.test.ts && tsx src/services/traffic-flow-dedup.test.ts && tsx src/services/traffic-flow-planes.test.ts && tsx src/services/traffic-flow-ip.test.ts && tsx src/services/traffic-flow-dest.test.ts && tsx src/services/traffic-flow-classify.test.ts && tsx src/services/traffic-flow-ripe.test.ts && tsx src/services/traffic-flow-brands.test.ts && tsx src/services/traffic-flow-ingest.test.ts && tsx src/services/traffic-flow-analytics.test.ts && tsx src/services/traffic-flow-map-hops.test.ts && tsx src/services/traffic-flow-purge.test.ts && tsx src/services/traffic-flow-geoip.test.ts && tsx src/services/traffic-flow-facts.test.ts && tsx src/services/traffic-flow-facts-filter.test.ts && tsx src/services/traffic-flow-facts-rebuild.test.ts && tsx src/services/statistics-aggregate.test.ts",
@@ -0,0 +1,19 @@
import assert from 'node:assert/strict'
import fs from 'node:fs'
import {createServer} from 'node:http'
import {randomBytes} from 'node:crypto'
const database=process.env.MMAPP_OWN_SQL_DB,url=new URL(process.env.DATABASE_URL)
assert(/^mm_accept_166_\d+$/.test(database??''));assert.equal(url.pathname,'/'+database);assert.equal(url.username,database)
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET=randomBytes(32).toString('hex');process.env.AUTH_ISSUER='https://own211.invalid';process.env.AUTH_PORTAL_URL='https://own211.invalid'
const {pool,closePool}=await import('/app/backend/dist/db/index.js'),{initDatabase}=await import('/app/backend/dist/db/bootstrap.js'),{buildApp}=await import('/app/backend/dist/index.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database);assert(Object.values((await pool.query('SELECT rolsuper,rolcreatedb,rolcreaterole,rolbypassrls FROM pg_roles WHERE rolname=current_user')).rows[0]).every(v=>v===false))
const psk='own211-'+randomBytes(24).toString('base64url'),baseline=false,cases=[];let app,serverId,nativeWrites=0,nativeCalls=0
const native=createServer((req,res)=>{nativeCalls++;if(req.method!=='GET')nativeWrites++;res.setHeader('content-type','application/json');res.end(JSON.stringify(req.url.endsWith('/interface/gre')?[{'.id':'*1',name:'own211-encrypted','local-address':'192.0.2.1','remote-address':'192.0.2.2','ipsec-secret':psk,mtu:'1476',keepalive:'10s,3',disabled:'false',running:'true','allow-fast-path':'false'},{'.id':'*2',name:'own211-plain','remote-address':'192.0.2.3','ipsec-secret':'',disabled:'false',running:'true'}]:[]))});await new Promise(r=>native.listen(0,'127.0.0.1',r))
const report={date:'2026-10-13',block:'12.5.211',database,baseline,cases,scope:'Production compiled canonical auth/RBAC using own restricted PostgreSQL, local native HTTP fixture and synthetic JWT/PSK',passed:false}
try{await initDatabase();serverId=Number((await pool.query("INSERT INTO servers(name,host,port,use_ssl,enabled,username,password) VALUES('own211-native','127.0.0.1',$1,false,true,'fixture','fixture') RETURNING id",[native.address().port])).rows[0].id);app=await buildApp({logger:false,startScheduler:false});await app.ready()
for(const path of ['/api/servers','/api/%73ervers','/%61pi/servers','/%61%70%69/servers','/api/gre/tunnels','/%61pi/gre/tunnels'])for(const method of ['GET','HEAD']){const before=nativeCalls;const r=await app.inject({method,url:path});assert.equal(r.statusCode,401);assert.equal(nativeCalls,before);cases.push({path,method,role:'anonymous',status:401})}
for(const path of ['/api/gre/tunnels','/api/%67re/tunnels','/%61pi/gre/tunnels'])for(const role of ['dashboard','reader','writer']){const permissions=role==='dashboard'?['mm:dashboard:read']:role==='reader'?['mm:network:read']:['mm:network:write'],token=app.jwt.sign({sub:'own211-'+role,apps:['mm'],permissions,iss:process.env.AUTH_ISSUER},{expiresIn:'5m'});const r=await app.inject({method:'GET',url:path+'?serverId='+serverId,headers:{authorization:'Bearer '+token}});assert.equal(r.statusCode,role==='dashboard'?403:200);if(role!=='dashboard')assert.equal(r.json().tunnels.find(t=>t.name==='own211-encrypted').ipsec.secret,role==='writer'?psk:'');cases.push({path,method:'GET',role,status:r.statusCode,privateMaterialStored:false});if(role!=='writer'){const patch=await app.inject({method:'PATCH',url:path,headers:{authorization:'Bearer '+token},payload:{serverId:String(serverId),rosId:'*1',comment:'not applied'}});assert.equal(patch.statusCode,403);cases.push({path,method:'PATCH',role,status:403})}}
for(const path of ['/api/auth/config','/%61pi/auth/config']){assert.equal((await app.inject({url:path})).statusCode,200);cases.push({path,role:'anonymous',status:200,publicConfig:true})}
assert.equal(nativeWrites,0);report.nativeWrites=0;report.passed=true
}catch(e){report.errorName=e.name;fs.writeFileSync('/app/backend/acceptance/gre211-private.log',String(e.stack??e),{mode:0o600})}finally{if(app)await app.close();native.closeAllConnections();await new Promise(r=>native.close(r));await closePool();fs.writeFileSync('/app/backend/acceptance/auth211-production-result.json',JSON.stringify(report,null,2));console.log(JSON.stringify(report))}
if(!report.passed)process.exitCode=1
+2 -8
View File
@@ -5,7 +5,7 @@ import { monitorEventLoopDelay } from "node:perf_hooks"
import { env } from "./config.js"
import { initDatabase } from "./db/bootstrap.js"
import { closePool } from "./db/index.js"
import authPlugin, { requireAuth } from "./plugins/auth.js"
import authPlugin, { requireApiAuth } from "./plugins/auth.js"
import { installDatabaseRequestGate } from "./plugins/database-request-gate.js"
import serversRoutes from "./routes/servers.js"
import bgpRoutes from "./routes/bgp.js"
@@ -108,13 +108,7 @@ export async function buildApp(opts?: {
}))
if (env.authRequired) {
app.addHook("preHandler", async (request, reply) => {
const pathname = request.url.split("?")[0] ?? request.url
if (!pathname.startsWith("/api/")) return
if (pathname === "/api/auth/config") return
await requireAuth(request, reply)
if (reply.sent) return
})
app.addHook("preHandler", requireApiAuth)
}
installDatabaseRequestGate(app)
+4 -4
View File
@@ -47,17 +47,17 @@ const RULES: Rule[] = [
{ methods: ["PUT", "POST"], match: path => path === "/api/geoip" || path === "/api/geoip/update", permission: "mm:traffic:write" },
{
methods: ["GET"],
match: p => /^\/api\/servers\/\d+\/ospf(?:\/|$)/.test(p),
match: p => /^\/api\/servers\/(?:\d+|:[A-Za-z]\w*)\/ospf(?:\/|$)/.test(p),
permission: "mm:network:read",
},
{
methods: ["POST"],
match: p => /^\/api\/servers\/\d+\/ospf\/optimize\/preview$/.test(p),
match: p => /^\/api\/servers\/(?:\d+|:[A-Za-z]\w*)\/ospf\/optimize\/preview$/.test(p),
permission: "mm:network:read",
},
{
methods: ["POST", "PUT", "PATCH", "DELETE"],
match: p => /^\/api\/servers\/\d+\/ospf(?:\/|$)/.test(p),
match: p => /^\/api\/servers\/(?:\d+|:[A-Za-z]\w*)\/ospf(?:\/|$)/.test(p),
permission: "mm:network:write",
},
@@ -201,7 +201,7 @@ export function permissionForRequest(
method: string,
path: string,
): string | null {
const m = method.toUpperCase()
const m = method.toUpperCase() === "HEAD" ? "GET" : method.toUpperCase()
const pathname = path.split("?")[0] ?? path
for (const rule of RULES) {
if (!rule.methods.includes(m)) continue
+16
View File
@@ -0,0 +1,16 @@
import assert from 'node:assert/strict'
import Fastify from 'fastify'
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET='own211-synthetic-secret';process.env.AUTH_ISSUER='https://own211.invalid';process.env.NODE_ENV='test'
const {default:authPlugin,requireApiAuth}=await import('./auth.js')
const app=Fastify();await app.register(authPlugin);app.addHook('preHandler',requireApiAuth);let calls=0
for(const url of ['/api/servers','/api/gre/tunnels','/api/servers/:id/ospf'])app.get(url,async()=>{calls++;return{ok:true}})
app.patch('/api/gre/tunnels',async()=>{calls++;return{ok:true}})
app.post('/api/servers/:id/ospf/optimize/preview',async()=>{calls++;return{ok:true}})
app.get('/api/auth/config',async()=>({public:true}));app.get('/health',async()=>({public:true}));await app.ready()
const token=(permissions:string[])=>app.jwt.sign({sub:'own211',apps:['mm'],permissions,iss:process.env.AUTH_ISSUER},{expiresIn:'5m'})
let cases=0
for(const url of ['/api/servers','/api/%73ervers','/%61pi/servers','/%61%70%69/%73ervers'])for(const method of ['GET','HEAD'] as const){const before=calls,r=await app.inject({method,url});assert.equal(r.statusCode,401,url);assert.equal(calls,before);cases++}
for(const url of ['/api/gre/tunnels','/api/%67re/tunnels','/%61pi/gre/tunnels'])for(const method of ['GET','HEAD','PATCH'] as const)for(const role of ['dashboard','reader','writer']){const before=calls,permissions=role==='dashboard'?['mm:dashboard:read']:role==='reader'?['mm:network:read']:['mm:network:write'],allowed=role==='writer'||(role==='reader'&&method!=='PATCH'),r=await app.inject({method,url,headers:{authorization:'Bearer '+token(permissions)}});assert.equal(r.statusCode,allowed?200:403,url+' '+method+' '+role);assert.equal(calls,before+(allowed?1:0));cases++}
for(const method of ['GET','POST'] as const){const url=method==='GET'?'/api/servers/1/ospf':'/api/servers/1/ospf/optimize/preview';assert.equal((await app.inject({method,url,headers:{authorization:'Bearer '+token(['mm:network:read'])}})).statusCode,200);assert.equal((await app.inject({method,url,headers:{authorization:'Bearer '+token(['mm:servers:read'])}})).statusCode,403);cases+=2}
for(const url of ['/health','/api/auth/config','/%61pi/auth/config']){assert.equal((await app.inject({url})).statusCode,200);cases++}
await app.close();console.log('Canonical API auth: '+cases+' cases, denied handlers not called, HEAD and OSPF matched templates preserved')
+8 -1
View File
@@ -74,7 +74,7 @@ export async function requireAuth(
): Promise<void> {
if (!env.authRequired) return
const pathname = (request.url.split("?")[0] ?? request.url)
const pathname = request.routeOptions.url ?? request.url.split("?")[0] ?? request.url
if (pathname === "/api/auth/config") return
const authHeader = request.headers.authorization ?? ""
@@ -116,4 +116,11 @@ export async function requireAuth(
}
}
/** Gate the matched route, including encoded static segments in the raw URL. */
export async function requireApiAuth(request: FastifyRequest, reply: FastifyReply): Promise<void> {
const pathname = request.routeOptions.url ?? request.url.split("?")[0] ?? request.url
if (!pathname.startsWith("/api/") || pathname === "/api/auth/config") return
await requireAuth(request, reply)
}
export default fp(authPlugin, { name: "auth" })
+16 -1
View File
@@ -1,6 +1,6 @@
# UX/UI: протокол внешней приёмки 12.5
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 12.5 открыт.
Актуальное продолжение 2026-10-13: блок 211 завершён. Закрыт анонимный обход JWT и section RBAC через encoded URL: проверяется маршрут Fastify, HEAD наследует GET policy, OSPF parameter routes сохраняют права. Пройдены auth42, production SQL/API29 и live26; тестовые DB/role/users удалены. Backend ux-auth211/source477/compiled1048, frontend ux-gre210. Следующий212 — совместимость GRE FastPath/PSK и шаблон экспорта недопустимого keepalive. Этап12.5 открыт.
IPsec78: реальный PSK responder established, mode-config198.18.78.10 и online=true подтверждены API; точные собственные объекты удалены. Сертификатный/DN online, late failure и history restore остаются открытыми. [Отчёт](ux-ui/acceptance/ipsec-mode-config-2026-10-09.json), [журнал](ux-ui-progress.md).
@@ -2556,3 +2556,18 @@ Runtime ux-gre210: backend healthy, source475/527 и compiled1048 matched. На
Протоколы: [native syntax](ux-ui/acceptance/gre210-native-range-2026-10-13.json), [дополнительный синтаксис](ux-ui/acceptance/gre210-native-range-extra-2026-10-13.json), [SQL/API14](ux-ui/acceptance/gre210-keepalive-result.json), [профили](ux-ui/acceptance/gre210-profile-2026-10-13.json), [E2E13](ux-ui/acceptance/gre210-e2e-2026-10-13.json), [native/UI/cleanup](ux-ui/acceptance/gre210-native-live-2026-10-13.json), [source](ux-ui/acceptance/source210-result.json), [compiled](ux-ui/acceptance/compiled210-result.json), [deployment](ux-ui/acceptance/gre210-deployment-2026-10-13.json).
## 12.5.211 — JWT/RBAC для закодированных URL (2026-10-13)
Actual baseline: обычный /api/servers без JWT возвращал401, но /%61pi/servers возвращал200 и server rows с непустыми паролями. Fastify распознаёт encoded static segments; middleware сравнивал raw URL и пропускал auth для encoded /api prefix. Для encoded имени раздела можно было также получить fallback dashboard permission. В evidence сохранены только статусы, число строк и boolean наличия пароля; raw body, credentials и JWT не сохранялись и не выводились.
Общий preHandler и requireAuth теперь используют request.routeOptions.url — фактически выбранный Fastify маршрут. Проверка public auth config/health сохранена; percent decoding не выполняется повторно вручную. Permission mapping OSPF принимает доверенные parameter templates наряду с прежними numeric paths; HEAD получает те же read permissions, что GET. Политики ролей и AUTH_REQUIRED=false не расширены. Frontend и RouterOS конфигурация не менялись.
42 канонических auth fixtures: encoded static prefix/раздел, GET/HEAD/PATCH, denied handler count0, OSPF reader/preview и servers-only denial, public config/health. Прежние test:auth, GeoIP JWT и NetFlow private export guards прошли; backend build прошёл. Изменена общая auth-основа, поэтому добавлены профильные проверки разных ресурсов и параметризованных маршрутов; полный UI regression192 не повторялся для backend-only изменения.
Production compiled SQL/API29 на собственной ограниченной DB/role: anonymous401, dashboard-only403, network reader/writer GET и PSK redaction, denied PATCH403 до native, public config200. Native writes0; DB/role/container удалены. Backend ux-auth211 развёрнут healthy; source477 и compiled1048 совпали. Frontend ux-gre210 сохранён. Live26 с настоящими Portal JWT: анонимные GET/HEAD401, обычные/encoded server и GRE GET по ролям, пустой PATCH403. Пустое тело гарантирует отсутствие native apply даже при ошибке защиты. Два временных Portal user удалены; секреты не публиковались. Пароли CHR не менялись.
Протоколы: [actual baseline](ux-ui/acceptance/auth211-baseline-live-2026-10-13.json), [профиль](ux-ui/acceptance/auth211-profile-2026-10-13.json), [production SQL/API29](ux-ui/acceptance/auth211-production-result.json), [live26](ux-ui/acceptance/auth211-live-2026-10-13.json), [source](ux-ui/acceptance/source211-result.json), [compiled](ux-ui/acceptance/compiled211-result.json), [deployment](ux-ui/acceptance/auth211-deployment-2026-10-13.json).
Следующий212: GRE create с PSK и опущенным FastPath, explicit incompatible combination, экспорт недопустимого/неизвестного keepalive. 12.5 остаётся открытой по внешней матрице.
+1 -1
View File
@@ -1,6 +1,6 @@
# План реализации UX/UI по пятичасовым окнам
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 12.5 открыт.
Актуальное продолжение 2026-10-13: блок 211 завершён. Закрыт анонимный обход JWT и section RBAC через encoded URL: проверяется маршрут Fastify, HEAD наследует GET policy, OSPF parameter routes сохраняют права. Пройдены auth42, production SQL/API29 и live26; тестовые DB/role/users удалены. Backend ux-auth211/source477/compiled1048, frontend ux-gre210. Следующий212 — совместимость GRE FastPath/PSK и шаблон экспорта недопустимого keepalive. Этап12.5 открыт.
Общий план проекта MikrotikManager. Основан на согласованном плане из задачи; точка продолжения обновлена 2026-10-13. Источник фактического прогресса, проверок и ограничений — [журнал реализации](ux-ui-progress.md). Этот документ задаёт объём и критерии завершения, журнал фиксирует результат.
+15 -1
View File
@@ -1,6 +1,6 @@
# UX/UI: журнал реализации
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 12.5 открыт.
Актуальное продолжение 2026-10-13: блок 211 завершён. Закрыт анонимный обход JWT и section RBAC через encoded URL: проверяется маршрут Fastify, HEAD наследует GET policy, OSPF parameter routes сохраняют права. Пройдены auth42, production SQL/API29 и live26; тестовые DB/role/users удалены. Backend ux-auth211/source477/compiled1048, frontend ux-gre210. Следующий212 — совместимость GRE FastPath/PSK и шаблон экспорта недопустимого keepalive. Этап12.5 открыт.
Общий план: [UX/UI по этапам и пятичасовым окнам](ux-ui-plan.md). План задаёт объём, этот журнал фиксирует выполненные блоки и ограничения. Записи расположены от новых к старым; подробная история этапа12 также сохранена в [ux-ui-stage-12.md](ux-ui-stage-12.md).
@@ -38,6 +38,20 @@
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Завершён в границах локальной приёмки 2026-10-03; ограничения и evidence в11Б.7 |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Локальная приёмка завершена в границах12.3.83; внешняя12.5 в работе, выполненные блоки1–196 и ограничения ниже. Runtime backend/frontend ux-peak196. |
## 12.5.211 — JWT/RBAC для закодированных URL (2026-10-13)
Actual baseline: обычный /api/servers без JWT возвращал401, но /%61pi/servers возвращал200 и server rows с непустыми паролями. Fastify распознаёт encoded static segments; middleware сравнивал raw URL и пропускал auth для encoded /api prefix. Для encoded имени раздела можно было также получить fallback dashboard permission. В evidence сохранены только статусы, число строк и boolean наличия пароля; raw body, credentials и JWT не сохранялись и не выводились.
Общий preHandler и requireAuth теперь используют request.routeOptions.url — фактически выбранный Fastify маршрут. Проверка public auth config/health сохранена; percent decoding не выполняется повторно вручную. Permission mapping OSPF принимает доверенные parameter templates наряду с прежними numeric paths; HEAD получает те же read permissions, что GET. Политики ролей и AUTH_REQUIRED=false не расширены. Frontend и RouterOS конфигурация не менялись.
42 канонических auth fixtures: encoded static prefix/раздел, GET/HEAD/PATCH, denied handler count0, OSPF reader/preview и servers-only denial, public config/health. Прежние test:auth, GeoIP JWT и NetFlow private export guards прошли; backend build прошёл. Изменена общая auth-основа, поэтому добавлены профильные проверки разных ресурсов и параметризованных маршрутов; полный UI regression192 не повторялся для backend-only изменения.
Production compiled SQL/API29 на собственной ограниченной DB/role: anonymous401, dashboard-only403, network reader/writer GET и PSK redaction, denied PATCH403 до native, public config200. Native writes0; DB/role/container удалены. Backend ux-auth211 развёрнут healthy; source477 и compiled1048 совпали. Frontend ux-gre210 сохранён. Live26 с настоящими Portal JWT: анонимные GET/HEAD401, обычные/encoded server и GRE GET по ролям, пустой PATCH403. Пустое тело гарантирует отсутствие native apply даже при ошибке защиты. Два временных Portal user удалены; секреты не публиковались. Пароли CHR не менялись.
Протоколы: [actual baseline](ux-ui/acceptance/auth211-baseline-live-2026-10-13.json), [профиль](ux-ui/acceptance/auth211-profile-2026-10-13.json), [production SQL/API29](ux-ui/acceptance/auth211-production-result.json), [live26](ux-ui/acceptance/auth211-live-2026-10-13.json), [source](ux-ui/acceptance/source211-result.json), [compiled](ux-ui/acceptance/compiled211-result.json), [deployment](ux-ui/acceptance/auth211-deployment-2026-10-13.json).
Следующий212: GRE create с PSK и опущенным FastPath, explicit incompatible combination, экспорт недопустимого/неизвестного keepalive. 12.5 остаётся открытой по внешней матрице.
## 12.5.210 — GRE keepalive и совместимость публичного каталога (2026-10-13)
На обоих CHR собственные отключённые интерфейсы подтвердили: native REST отклоняет 0, none, пустую строку, disabled, no и ноль попыток. Дробные интервалы принимаются, но сокращаются RouterOS до целых секунд. Поэтому форма и write schema требуют целые значения 1..4294967295 вместо обещания «0 = откл.». Неверный ввод сохраняет форму и не отправляет запрос. Официальный default 10s,10: [MikroTik GRE](https://help.mikrotik.com/docs/spaces/ROS/pages/24805531/GRE).
+19 -4
View File
@@ -1,10 +1,10 @@
# Оставшаяся внешняя приёмка — 2026-10-13
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 12.5 открыт.
Актуальное продолжение 2026-10-13: блок 211 завершён. Закрыт анонимный обход JWT и section RBAC через encoded URL: проверяется маршрут Fastify, HEAD наследует GET policy, OSPF parameter routes сохраняют права. Пройдены auth42, production SQL/API29 и live26; тестовые DB/role/users удалены. Backend ux-auth211/source477/compiled1048, frontend ux-gre210. Следующий212 — совместимость GRE FastPath/PSK и шаблон экспорта недопустимого keepalive. Этап12.5 открыт.
Это актуальная матрица после 12.5.210. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
Это актуальная матрица после 12.5.211. Исторические записи сохраняют версии и ограничения на момент выполнения; поздние проверки ниже уточняют их. Этап 12.5 открыт. Успех в двух CHR и одном backend не означает приёмку всех конфигураций, интеграций или полной регрессии.
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend/frontend `ux-gre210`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
Стенд: Debian13/Compose, PostgreSQL18.6, два CHR RouterOS7.24.5. Backend `ux-auth211`, frontend `ux-gre210`; production lab с195 использует AUTH_REQUIRED=true и Portal1.3.1. Подписанные JWT/RBAC проверялись в изолированных процессах. Все собственные объекты112 удалены, исходные13 коллекций×2 (включая logging/certificates/files/routes) проверены; пароли не менялись.
| Раздел | Подтверждено | Осталось / условие завершения |
| --- | --- | --- |
@@ -26,7 +26,7 @@
## Следующая последовательность — 2026-10-13
1. **211 — canonical auth path:** закрыть обход JWT/RBAC через encoded API prefix; actual anonymous baseline200 сохранён без raw body/секретов. После него — FastPath/PSK default и экспорт неизвестного keepalive.
1. **212 — GRE compatibility:** PSK/FastPath default и explicit conflict; экспорт недопустимого/неизвестного keepalive. Canonical auth/RBAC211 закрыт и развёрнут.
2. **Профильный контроль:** для новых изменений выполнять затронутые SQL/API/UI проверки. Общая регрессия192 выполнена; не повторять её без изменений общей основы или нового основания.
3. **Внешние зависимости:** Portal issuer/login/revoke, Telegram bot/chat/разрешённое сообщение, клиентские ОС, независимые WAN/public IPv6 и scale/multi-instance — отдельные группы по таблице выше. Ограничения текущего стенда не превращать в обещание приёмки всех конфигураций.
@@ -1474,3 +1474,18 @@ Runtime ux-gre210: backend healthy, source475/527 и compiled1048 matched. На
Протоколы: [native syntax](ux-ui/acceptance/gre210-native-range-2026-10-13.json), [дополнительный синтаксис](ux-ui/acceptance/gre210-native-range-extra-2026-10-13.json), [SQL/API14](ux-ui/acceptance/gre210-keepalive-result.json), [профили](ux-ui/acceptance/gre210-profile-2026-10-13.json), [E2E13](ux-ui/acceptance/gre210-e2e-2026-10-13.json), [native/UI/cleanup](ux-ui/acceptance/gre210-native-live-2026-10-13.json), [source](ux-ui/acceptance/source210-result.json), [compiled](ux-ui/acceptance/compiled210-result.json), [deployment](ux-ui/acceptance/gre210-deployment-2026-10-13.json).
## 12.5.211 — JWT/RBAC для закодированных URL (2026-10-13)
Actual baseline: обычный /api/servers без JWT возвращал401, но /%61pi/servers возвращал200 и server rows с непустыми паролями. Fastify распознаёт encoded static segments; middleware сравнивал raw URL и пропускал auth для encoded /api prefix. Для encoded имени раздела можно было также получить fallback dashboard permission. В evidence сохранены только статусы, число строк и boolean наличия пароля; raw body, credentials и JWT не сохранялись и не выводились.
Общий preHandler и requireAuth теперь используют request.routeOptions.url — фактически выбранный Fastify маршрут. Проверка public auth config/health сохранена; percent decoding не выполняется повторно вручную. Permission mapping OSPF принимает доверенные parameter templates наряду с прежними numeric paths; HEAD получает те же read permissions, что GET. Политики ролей и AUTH_REQUIRED=false не расширены. Frontend и RouterOS конфигурация не менялись.
42 канонических auth fixtures: encoded static prefix/раздел, GET/HEAD/PATCH, denied handler count0, OSPF reader/preview и servers-only denial, public config/health. Прежние test:auth, GeoIP JWT и NetFlow private export guards прошли; backend build прошёл. Изменена общая auth-основа, поэтому добавлены профильные проверки разных ресурсов и параметризованных маршрутов; полный UI regression192 не повторялся для backend-only изменения.
Production compiled SQL/API29 на собственной ограниченной DB/role: anonymous401, dashboard-only403, network reader/writer GET и PSK redaction, denied PATCH403 до native, public config200. Native writes0; DB/role/container удалены. Backend ux-auth211 развёрнут healthy; source477 и compiled1048 совпали. Frontend ux-gre210 сохранён. Live26 с настоящими Portal JWT: анонимные GET/HEAD401, обычные/encoded server и GRE GET по ролям, пустой PATCH403. Пустое тело гарантирует отсутствие native apply даже при ошибке защиты. Два временных Portal user удалены; секреты не публиковались. Пароли CHR не менялись.
Протоколы: [actual baseline](ux-ui/acceptance/auth211-baseline-live-2026-10-13.json), [профиль](ux-ui/acceptance/auth211-profile-2026-10-13.json), [production SQL/API29](ux-ui/acceptance/auth211-production-result.json), [live26](ux-ui/acceptance/auth211-live-2026-10-13.json), [source](ux-ui/acceptance/source211-result.json), [compiled](ux-ui/acceptance/compiled211-result.json), [deployment](ux-ui/acceptance/auth211-deployment-2026-10-13.json).
Следующий212: GRE create с PSK и опущенным FastPath, explicit incompatible combination, экспорт недопустимого/неизвестного keepalive. 12.5 остаётся открытой по внешней матрице.
+16 -1
View File
@@ -1,6 +1,6 @@
# Этап 12: итоговая приёмка UX/UI
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 12.5 открыт.
Актуальное продолжение 2026-10-13: блок 211 завершён. Закрыт анонимный обход JWT и section RBAC через encoded URL: проверяется маршрут Fastify, HEAD наследует GET policy, OSPF parameter routes сохраняют права. Пройдены auth42, production SQL/API29 и live26; тестовые DB/role/users удалены. Backend ux-auth211/source477/compiled1048, frontend ux-gre210. Следующий212 — совместимость GRE FastPath/PSK и шаблон экспорта недопустимого keepalive. Этап12.5 открыт.
План: [ux-ui-plan.md](ux-ui-plan.md). Фактические результаты: [ux-ui-progress.md](ux-ui-progress.md). Подготовлен после сводных проверок 11Б; завершение каждого блока требует локального коммита без push.
@@ -3863,3 +3863,18 @@ Runtime ux-gre210: backend healthy, source475/527 и compiled1048 matched. На
Протоколы: [native syntax](ux-ui/acceptance/gre210-native-range-2026-10-13.json), [дополнительный синтаксис](ux-ui/acceptance/gre210-native-range-extra-2026-10-13.json), [SQL/API14](ux-ui/acceptance/gre210-keepalive-result.json), [профили](ux-ui/acceptance/gre210-profile-2026-10-13.json), [E2E13](ux-ui/acceptance/gre210-e2e-2026-10-13.json), [native/UI/cleanup](ux-ui/acceptance/gre210-native-live-2026-10-13.json), [source](ux-ui/acceptance/source210-result.json), [compiled](ux-ui/acceptance/compiled210-result.json), [deployment](ux-ui/acceptance/gre210-deployment-2026-10-13.json).
## 12.5.211 — JWT/RBAC для закодированных URL (2026-10-13)
Actual baseline: обычный /api/servers без JWT возвращал401, но /%61pi/servers возвращал200 и server rows с непустыми паролями. Fastify распознаёт encoded static segments; middleware сравнивал raw URL и пропускал auth для encoded /api prefix. Для encoded имени раздела можно было также получить fallback dashboard permission. В evidence сохранены только статусы, число строк и boolean наличия пароля; raw body, credentials и JWT не сохранялись и не выводились.
Общий preHandler и requireAuth теперь используют request.routeOptions.url — фактически выбранный Fastify маршрут. Проверка public auth config/health сохранена; percent decoding не выполняется повторно вручную. Permission mapping OSPF принимает доверенные parameter templates наряду с прежними numeric paths; HEAD получает те же read permissions, что GET. Политики ролей и AUTH_REQUIRED=false не расширены. Frontend и RouterOS конфигурация не менялись.
42 канонических auth fixtures: encoded static prefix/раздел, GET/HEAD/PATCH, denied handler count0, OSPF reader/preview и servers-only denial, public config/health. Прежние test:auth, GeoIP JWT и NetFlow private export guards прошли; backend build прошёл. Изменена общая auth-основа, поэтому добавлены профильные проверки разных ресурсов и параметризованных маршрутов; полный UI regression192 не повторялся для backend-only изменения.
Production compiled SQL/API29 на собственной ограниченной DB/role: anonymous401, dashboard-only403, network reader/writer GET и PSK redaction, denied PATCH403 до native, public config200. Native writes0; DB/role/container удалены. Backend ux-auth211 развёрнут healthy; source477 и compiled1048 совпали. Frontend ux-gre210 сохранён. Live26 с настоящими Portal JWT: анонимные GET/HEAD401, обычные/encoded server и GRE GET по ролям, пустой PATCH403. Пустое тело гарантирует отсутствие native apply даже при ошибке защиты. Два временных Portal user удалены; секреты не публиковались. Пароли CHR не менялись.
Протоколы: [actual baseline](ux-ui/acceptance/auth211-baseline-live-2026-10-13.json), [профиль](ux-ui/acceptance/auth211-profile-2026-10-13.json), [production SQL/API29](ux-ui/acceptance/auth211-production-result.json), [live26](ux-ui/acceptance/auth211-live-2026-10-13.json), [source](ux-ui/acceptance/source211-result.json), [compiled](ux-ui/acceptance/compiled211-result.json), [deployment](ux-ui/acceptance/auth211-deployment-2026-10-13.json).
Следующий212: GRE create с PSK и опущенным FastPath, explicit incompatible combination, экспорт недопустимого/неизвестного keepalive. 12.5 остаётся открытой по внешней матрице.
+16 -1
View File
@@ -1,6 +1,6 @@
# Тестовый сервер UX/UI — 2026-10-10
Актуальное продолжение 2026-10-13: блок 210 завершён. GRE создаётся с default keepalive 10s,10; минутные значения читаются в секундах, PATCH другого поля сохраняет native timer. Валидатор каталога IPsec исправлен для Filters/карты/диагностики. Пройдены SQL/API14, E2E13 и live GRE/Filters UI4; собственные интерфейсы удалены. Backend/frontend ux-gre210, source475/527 и compiled1048 совпали. Срочный следующий блок 211 — auth/RBAC для закодированных URL; baseline анонимного обхода подтверждён. Этап 12.5 открыт.
Актуальное продолжение 2026-10-13: блок 211 завершён. Закрыт анонимный обход JWT и section RBAC через encoded URL: проверяется маршрут Fastify, HEAD наследует GET policy, OSPF parameter routes сохраняют права. Пройдены auth42, production SQL/API29 и live26; тестовые DB/role/users удалены. Backend ux-auth211/source477/compiled1048, frontend ux-gre210. Следующий212 — совместимость GRE FastPath/PSK и шаблон экспорта недопустимого keepalive. Этап12.5 открыт.
| Параметр | Проверенное состояние |
| --- | --- |
@@ -2287,3 +2287,18 @@ Runtime ux-gre210: backend healthy, source475/527 и compiled1048 matched. На
Протоколы: [native syntax](ux-ui/acceptance/gre210-native-range-2026-10-13.json), [дополнительный синтаксис](ux-ui/acceptance/gre210-native-range-extra-2026-10-13.json), [SQL/API14](ux-ui/acceptance/gre210-keepalive-result.json), [профили](ux-ui/acceptance/gre210-profile-2026-10-13.json), [E2E13](ux-ui/acceptance/gre210-e2e-2026-10-13.json), [native/UI/cleanup](ux-ui/acceptance/gre210-native-live-2026-10-13.json), [source](ux-ui/acceptance/source210-result.json), [compiled](ux-ui/acceptance/compiled210-result.json), [deployment](ux-ui/acceptance/gre210-deployment-2026-10-13.json).
## 12.5.211 — JWT/RBAC для закодированных URL (2026-10-13)
Actual baseline: обычный /api/servers без JWT возвращал401, но /%61pi/servers возвращал200 и server rows с непустыми паролями. Fastify распознаёт encoded static segments; middleware сравнивал raw URL и пропускал auth для encoded /api prefix. Для encoded имени раздела можно было также получить fallback dashboard permission. В evidence сохранены только статусы, число строк и boolean наличия пароля; raw body, credentials и JWT не сохранялись и не выводились.
Общий preHandler и requireAuth теперь используют request.routeOptions.url — фактически выбранный Fastify маршрут. Проверка public auth config/health сохранена; percent decoding не выполняется повторно вручную. Permission mapping OSPF принимает доверенные parameter templates наряду с прежними numeric paths; HEAD получает те же read permissions, что GET. Политики ролей и AUTH_REQUIRED=false не расширены. Frontend и RouterOS конфигурация не менялись.
42 канонических auth fixtures: encoded static prefix/раздел, GET/HEAD/PATCH, denied handler count0, OSPF reader/preview и servers-only denial, public config/health. Прежние test:auth, GeoIP JWT и NetFlow private export guards прошли; backend build прошёл. Изменена общая auth-основа, поэтому добавлены профильные проверки разных ресурсов и параметризованных маршрутов; полный UI regression192 не повторялся для backend-only изменения.
Production compiled SQL/API29 на собственной ограниченной DB/role: anonymous401, dashboard-only403, network reader/writer GET и PSK redaction, denied PATCH403 до native, public config200. Native writes0; DB/role/container удалены. Backend ux-auth211 развёрнут healthy; source477 и compiled1048 совпали. Frontend ux-gre210 сохранён. Live26 с настоящими Portal JWT: анонимные GET/HEAD401, обычные/encoded server и GRE GET по ролям, пустой PATCH403. Пустое тело гарантирует отсутствие native apply даже при ошибке защиты. Два временных Portal user удалены; секреты не публиковались. Пароли CHR не менялись.
Протоколы: [actual baseline](ux-ui/acceptance/auth211-baseline-live-2026-10-13.json), [профиль](ux-ui/acceptance/auth211-profile-2026-10-13.json), [production SQL/API29](ux-ui/acceptance/auth211-production-result.json), [live26](ux-ui/acceptance/auth211-live-2026-10-13.json), [source](ux-ui/acceptance/source211-result.json), [compiled](ux-ui/acceptance/compiled211-result.json), [deployment](ux-ui/acceptance/auth211-deployment-2026-10-13.json).
Следующий212: GRE create с PSK и опущенным FastPath, explicit incompatible combination, экспорт недопустимого/неизвестного keepalive. 12.5 остаётся открытой по внешней матрице.
@@ -0,0 +1,30 @@
{
"date": "2026-10-13",
"block": "12.5.211",
"runtime": "ux-gre209",
"cases": [
{
"path": "/api/servers",
"status": 401,
"anonymous": true,
"serverRows": 0,
"nonemptyPasswordPresent": false
},
{
"path": "/api/%73ervers",
"status": 401,
"anonymous": true,
"serverRows": 0,
"nonemptyPasswordPresent": false
},
{
"path": "/%61pi/servers",
"status": 200,
"anonymous": true,
"serverRows": 2,
"nonemptyPasswordPresent": true
}
],
"rawBodyStored": false,
"mutationRequests": 0
}
@@ -0,0 +1,24 @@
{
"date": "2026-10-13",
"block": "12.5.211",
"services": {
"backend": {
"image": "mmapp-test/backend:ux-auth211",
"imageId": "sha256:904c844a729465e8cdb48278a1f2829c88fe2764941ae60b4a0d05dc2f2d3b18",
"running": true,
"health": "healthy"
},
"frontend": {
"image": "mmapp-test/frontend:ux-gre210",
"imageId": "sha256:b4b1280f729e89de83b30bd242d37bda831306720b3c78cb9eef6048c3e3178e",
"running": true,
"health": null
}
},
"rollbackTags": {
"backend": "ux-gre210",
"frontend": "ux-gre210"
},
"backendSourceMatched": 477,
"compiledMatched": 1048
}
@@ -0,0 +1,193 @@
{
"date": "2026-10-13",
"block": "12.5.211",
"backend": "ux-auth211",
"frontend": "ux-gre210",
"cases": [
{
"role": "anonymous",
"path": "/api/servers",
"method": "GET",
"status": 401
},
{
"role": "anonymous",
"path": "/api/servers",
"method": "HEAD",
"status": 401
},
{
"role": "anonymous",
"path": "/api/%73ervers",
"method": "GET",
"status": 401
},
{
"role": "anonymous",
"path": "/api/%73ervers",
"method": "HEAD",
"status": 401
},
{
"role": "anonymous",
"path": "/%61pi/servers",
"method": "GET",
"status": 401
},
{
"role": "anonymous",
"path": "/%61pi/servers",
"method": "HEAD",
"status": 401
},
{
"role": "anonymous",
"path": "/%61%70%69/servers",
"method": "GET",
"status": 401
},
{
"role": "anonymous",
"path": "/%61%70%69/servers",
"method": "HEAD",
"status": 401
},
{
"role": "dashboard",
"path": "/api/servers",
"method": "GET",
"status": 403,
"privateMaterialAbsent": true
},
{
"role": "dashboard",
"path": "/api/%73ervers",
"method": "GET",
"status": 403,
"privateMaterialAbsent": true
},
{
"role": "dashboard",
"path": "/%61pi/servers",
"method": "GET",
"status": 403,
"privateMaterialAbsent": true
},
{
"role": "dashboard",
"path": "/api/gre/tunnels",
"method": "GET",
"status": 403,
"pskAbsent": true
},
{
"role": "dashboard",
"path": "/api/gre/tunnels",
"method": "PATCH",
"status": 403,
"emptyBody": true,
"nativeApplication": false
},
{
"role": "dashboard",
"path": "/api/%67re/tunnels",
"method": "GET",
"status": 403,
"pskAbsent": true
},
{
"role": "dashboard",
"path": "/api/%67re/tunnels",
"method": "PATCH",
"status": 403,
"emptyBody": true,
"nativeApplication": false
},
{
"role": "dashboard",
"path": "/%61pi/gre/tunnels",
"method": "GET",
"status": 403,
"pskAbsent": true
},
{
"role": "dashboard",
"path": "/%61pi/gre/tunnels",
"method": "PATCH",
"status": 403,
"emptyBody": true,
"nativeApplication": false
},
{
"role": "reader",
"path": "/api/servers",
"method": "GET",
"status": 200,
"privateMaterialAbsent": true
},
{
"role": "reader",
"path": "/api/%73ervers",
"method": "GET",
"status": 200,
"privateMaterialAbsent": true
},
{
"role": "reader",
"path": "/%61pi/servers",
"method": "GET",
"status": 200,
"privateMaterialAbsent": true
},
{
"role": "reader",
"path": "/api/gre/tunnels",
"method": "GET",
"status": 200,
"pskAbsent": true
},
{
"role": "reader",
"path": "/api/gre/tunnels",
"method": "PATCH",
"status": 403,
"emptyBody": true,
"nativeApplication": false
},
{
"role": "reader",
"path": "/api/%67re/tunnels",
"method": "GET",
"status": 200,
"pskAbsent": true
},
{
"role": "reader",
"path": "/api/%67re/tunnels",
"method": "PATCH",
"status": 403,
"emptyBody": true,
"nativeApplication": false
},
{
"role": "reader",
"path": "/%61pi/gre/tunnels",
"method": "GET",
"status": 200,
"pskAbsent": true
},
{
"role": "reader",
"path": "/%61pi/gre/tunnels",
"method": "PATCH",
"status": 403,
"emptyBody": true,
"nativeApplication": false
}
],
"publicAuthConfig": 200,
"realPortalJWT": true,
"rawResponseBodiesStored": false,
"ownedUsersRemoved": true,
"scope": "Anonymous GET/HEAD and actual restricted roles; denied PATCH uses empty body and cannot apply native objects"
}
@@ -0,0 +1,195 @@
{
"date": "2026-10-13",
"block": "12.5.211",
"database": "mm_accept_166_1791901195054",
"baseline": false,
"cases": [
{
"path": "/api/servers",
"method": "GET",
"role": "anonymous",
"status": 401
},
{
"path": "/api/servers",
"method": "HEAD",
"role": "anonymous",
"status": 401
},
{
"path": "/api/%73ervers",
"method": "GET",
"role": "anonymous",
"status": 401
},
{
"path": "/api/%73ervers",
"method": "HEAD",
"role": "anonymous",
"status": 401
},
{
"path": "/%61pi/servers",
"method": "GET",
"role": "anonymous",
"status": 401
},
{
"path": "/%61pi/servers",
"method": "HEAD",
"role": "anonymous",
"status": 401
},
{
"path": "/%61%70%69/servers",
"method": "GET",
"role": "anonymous",
"status": 401
},
{
"path": "/%61%70%69/servers",
"method": "HEAD",
"role": "anonymous",
"status": 401
},
{
"path": "/api/gre/tunnels",
"method": "GET",
"role": "anonymous",
"status": 401
},
{
"path": "/api/gre/tunnels",
"method": "HEAD",
"role": "anonymous",
"status": 401
},
{
"path": "/%61pi/gre/tunnels",
"method": "GET",
"role": "anonymous",
"status": 401
},
{
"path": "/%61pi/gre/tunnels",
"method": "HEAD",
"role": "anonymous",
"status": 401
},
{
"path": "/api/gre/tunnels",
"method": "GET",
"role": "dashboard",
"status": 403,
"privateMaterialStored": false
},
{
"path": "/api/gre/tunnels",
"method": "PATCH",
"role": "dashboard",
"status": 403
},
{
"path": "/api/gre/tunnels",
"method": "GET",
"role": "reader",
"status": 200,
"privateMaterialStored": false
},
{
"path": "/api/gre/tunnels",
"method": "PATCH",
"role": "reader",
"status": 403
},
{
"path": "/api/gre/tunnels",
"method": "GET",
"role": "writer",
"status": 200,
"privateMaterialStored": false
},
{
"path": "/api/%67re/tunnels",
"method": "GET",
"role": "dashboard",
"status": 403,
"privateMaterialStored": false
},
{
"path": "/api/%67re/tunnels",
"method": "PATCH",
"role": "dashboard",
"status": 403
},
{
"path": "/api/%67re/tunnels",
"method": "GET",
"role": "reader",
"status": 200,
"privateMaterialStored": false
},
{
"path": "/api/%67re/tunnels",
"method": "PATCH",
"role": "reader",
"status": 403
},
{
"path": "/api/%67re/tunnels",
"method": "GET",
"role": "writer",
"status": 200,
"privateMaterialStored": false
},
{
"path": "/%61pi/gre/tunnels",
"method": "GET",
"role": "dashboard",
"status": 403,
"privateMaterialStored": false
},
{
"path": "/%61pi/gre/tunnels",
"method": "PATCH",
"role": "dashboard",
"status": 403
},
{
"path": "/%61pi/gre/tunnels",
"method": "GET",
"role": "reader",
"status": 200,
"privateMaterialStored": false
},
{
"path": "/%61pi/gre/tunnels",
"method": "PATCH",
"role": "reader",
"status": 403
},
{
"path": "/%61pi/gre/tunnels",
"method": "GET",
"role": "writer",
"status": 200,
"privateMaterialStored": false
},
{
"path": "/api/auth/config",
"role": "anonymous",
"status": 200,
"publicConfig": true
},
{
"path": "/%61pi/auth/config",
"role": "anonymous",
"status": 200,
"publicConfig": true
}
],
"scope": "Production compiled canonical auth/RBAC using own restricted PostgreSQL, local native HTTP fixture and synthetic JWT/PSK",
"passed": true,
"nativeWrites": 0,
"databaseAndRoleRemoved": true
}
@@ -0,0 +1,13 @@
{
"date": "2026-10-13",
"block": "12.5.211",
"canonicalAuthCases": 42,
"commands": [
"npm --prefix backend run test:auth",
"npx tsx backend/src/plugins/auth.geoip.test.ts",
"npx tsx backend/src/plugins/auth.netflow-settings.test.ts"
],
"passed": true,
"backendBuildPassed": true,
"scope": "Matched static/parameter routes, GET/HEAD/PATCH, OSPF read preview, public auth config/health, existing auth/GeoIP/private NetFlow export policy"
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,10 @@
{
"date": "2026-10-13",
"block": "12.5.211",
"kinds": [
{
"kind": "backend",
"matched": 477
}
]
}