fix(auth): скрыть пароли от читателя и согласовать Portal

This commit is contained in:
Denozordec committed 2026-10-13 15:35:31 +07:00
1 parent a15917d08b
commit 608f743003
20 files changed
+1412 -19

No files matched your search

@@ -1,6 +1,12 @@
import { parseJsonArray } from "../../../db/json.js"
import type { ServerRead, WanUplinkRead } from "../../../types/server.js"
import type { ServerRow, SnapshotRow } from "../repository/servers-repository.js"
import {hasPermission,type AuthUser} from "../../../lib/permissions.js"
/** Reading inventory does not grant access to credentials used by device operations. */
export function serverReadForUser(server:ServerRead,user?:AuthUser):ServerRead {
return !user || hasPermission(user.permissions,"mm:servers:write") ? server : {...server,password:""}
}
function parseWanUplinksJson(raw: unknown): WanUplinkRead[] {
const data = parseJsonArray(raw)
@@ -0,0 +1,28 @@
import assert from 'node:assert/strict'
import {SignJWT} from 'jose'
const database=process.env.MMAPP_OWN_SQL_DB
if(!database)console.log('server secrets SQL: skipped; explicit owned database required')
else{
assert(/^mm_accept_166_\d+$/.test(database));assert.equal(new URL(process.env.DATABASE_URL!).pathname,'/'+database)
process.env.AUTH_REQUIRED='true';process.env.AUTH_JWT_SECRET='owned196-server-read-secret';process.env.AUTH_ISSUER='https://owned196.test';process.env.NODE_ENV='test'
const {pool,closePool}=await import('../db/index.js');const {initDatabase}=await import('../db/bootstrap.js');const {buildApp}=await import('../index.js')
assert.equal((await pool.query('SELECT current_database() AS name')).rows[0].name,database)
let serverId:number|undefined;const app=await buildApp({logger:false,startScheduler:false})
try{
await initDatabase()
serverId=Number((await pool.query("INSERT INTO servers(name,host,enabled,username,password) VALUES('own196-secret','192.0.2.196',false,'fixture','own196-password') RETURNING id")).rows[0].id)
for(const permission of ['mm:servers:read','mm:servers:write','mm:servers:admin']){
const token=await new SignJWT({sub:'own196',apps:['mm'],permissions:[permission]}).setProtectedHeader({alg:'HS256'}).setIssuer('https://owned196.test').setExpirationTime('1h').sign(new TextEncoder().encode('owned196-server-read-secret'))
for(const url of ['/api/servers','/api/servers/'+serverId]){
const r=await app.inject({method:'GET',url,headers:{authorization:'Bearer '+token}});assert.equal(r.statusCode,200)
const body=r.json(),row=Array.isArray(body)?body.find(s=>s.id===serverId):body
assert.equal(row.password,permission.endsWith(':read')?'':'own196-password');assert.equal(row.username,'fixture')
}
}
assert.equal((await pool.query('SELECT password FROM servers WHERE id=$1',[serverId])).rows[0].password,'own196-password','projection never changes stored credentials')
const noToken=await app.inject({method:'GET',url:'/api/servers'});assert.equal(noToken.statusCode,401)
console.log('server secrets owned SQL: reader/writer/admin list/detail and unchanged stored password passed')
}finally{
await app.close();if(serverId!==undefined)await pool.query('DELETE FROM servers WHERE id=$1',[serverId]);await closePool()
}
}
+4 -3
View File
@@ -23,6 +23,7 @@ import {
updateServer,
} from "../modules/servers/service/servers-service.js"
import { getServerRowById } from "../modules/servers/repository/servers-repository.js"
import {serverReadForUser} from "../modules/servers/mapper/servers-mapper.js"
// ── plugin ─────────────────────────────────────────────────────────────────────
@@ -76,8 +77,8 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
})
// GET /api/servers
app.get("/", async (_req, reply) => {
return reply.send(await listServersRead())
app.get("/", async (req, reply) => {
return reply.send((await listServersRead()).map(server=>serverReadForUser(server,req.authUser)))
})
// GET /api/servers/:id/ros-src-address — IPv4 для src-address в RouterOS (не FQDN)
@@ -238,7 +239,7 @@ const serversRoutes: FastifyPluginAsyncZod = async (app) => {
const params = req.params as ServerIdParams
const server = await getServerReadById(params.id)
if (!server) return reply.status(404).send({ error: "Server not found" })
return reply.send(server)
return reply.send(serverReadForUser(server,req.authUser))
})
// PUT /api/servers/:id