feat: реализовать EvoFirewall V1 control plane
API, UI, Linux/MikroTik agents, IP lists, политики, stats, CI и интеграция с auth-portal/EvoBGP. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,217 @@
|
||||
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
|
||||
import fp from 'fastify-plugin'
|
||||
import {
|
||||
hasPermission,
|
||||
permissionForRequest,
|
||||
type AuthUser,
|
||||
} from '@evofw/shared'
|
||||
import type { AppConfig } from '../config.js'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { repos } from '@evofw/db'
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyRequest {
|
||||
authUser?: AuthUser
|
||||
agentId?: string
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@fastify/jwt' {
|
||||
interface FastifyJWT {
|
||||
payload: {
|
||||
sub: string
|
||||
email?: string
|
||||
name?: string
|
||||
apps?: string[]
|
||||
permissions?: string[]
|
||||
is_admin?: boolean
|
||||
iss?: string
|
||||
}
|
||||
user: {
|
||||
sub: string
|
||||
email?: string
|
||||
name?: string
|
||||
apps?: string[]
|
||||
permissions?: string[]
|
||||
is_admin?: boolean
|
||||
iss?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hashToken(token: string): string {
|
||||
return createHash('sha256').update(token).digest('hex')
|
||||
}
|
||||
|
||||
function isPublicPath(url: string): boolean {
|
||||
const path = url.split('?')[0] ?? url
|
||||
if (path === '/health' || path === '/ready') return true
|
||||
if (path === '/api/auth/config' || path === '/api/v1/auth/config') return true
|
||||
if (path.startsWith('/v1/agent/enroll')) return true
|
||||
if (path.startsWith('/v1/agent/install')) return true
|
||||
if (path.startsWith('/v1/agent/sync-script')) return true
|
||||
if (path.startsWith('/v1/agent/mikrotik')) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function isAgentPath(url: string): boolean {
|
||||
const path = url.split('?')[0] ?? url
|
||||
return (
|
||||
path === '/v1/agent/policy' ||
|
||||
path === '/v1/agent/apply-report' ||
|
||||
path === '/v1/agent/heartbeat'
|
||||
)
|
||||
}
|
||||
|
||||
async function authPlugin(
|
||||
app: FastifyInstance,
|
||||
opts: { config: AppConfig },
|
||||
) {
|
||||
const { config } = opts
|
||||
|
||||
app.get('/api/auth/config', async () => ({
|
||||
required: config.authRequired,
|
||||
portal_url: config.authPortalUrl,
|
||||
}))
|
||||
app.get('/api/v1/auth/config', async () => ({
|
||||
required: config.authRequired,
|
||||
portal_url: config.authPortalUrl,
|
||||
}))
|
||||
|
||||
if (config.authRequired) {
|
||||
if (!config.jwtSecret || config.jwtSecret.length < 8) {
|
||||
throw new Error(
|
||||
'AUTH_JWT_SECRET / JWT_SECRET required when AUTH_REQUIRED=true',
|
||||
)
|
||||
}
|
||||
await app.register(import('@fastify/jwt'), {
|
||||
secret: config.jwtSecret,
|
||||
verify: { allowedIss: [config.authIssuer] },
|
||||
})
|
||||
} else {
|
||||
await app.register(import('@fastify/jwt'), {
|
||||
secret: config.jwtSecret || 'dev-secret-change-me',
|
||||
})
|
||||
}
|
||||
|
||||
app.decorate(
|
||||
'requireAuth',
|
||||
async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
if (!config.authRequired) {
|
||||
request.authUser = {
|
||||
id: 'dev',
|
||||
email: 'dev@local',
|
||||
name: 'Dev',
|
||||
apps: ['fw'],
|
||||
permissions: [
|
||||
'fw:dashboard:read',
|
||||
'fw:agents:write',
|
||||
'fw:lists:write',
|
||||
'fw:policies:write',
|
||||
'fw:stats:read',
|
||||
'fw:settings:admin',
|
||||
],
|
||||
isAdmin: true,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
} catch {
|
||||
return reply.code(401).send({
|
||||
error: { code: 'UNAUTHORIZED', message: 'Требуется авторизация' },
|
||||
})
|
||||
}
|
||||
|
||||
const payload = request.user
|
||||
const apps = Array.isArray(payload.apps)
|
||||
? payload.apps.map(String)
|
||||
: []
|
||||
const permissions = Array.isArray(payload.permissions)
|
||||
? payload.permissions.map(String)
|
||||
: []
|
||||
|
||||
if (!apps.includes('fw') && !payload.is_admin) {
|
||||
return reply.code(403).send({
|
||||
error: {
|
||||
code: 'FORBIDDEN',
|
||||
message: 'Нет доступа к приложению EvoFirewall',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
request.authUser = {
|
||||
id: String(payload.sub),
|
||||
email: String(payload.email ?? ''),
|
||||
name: String(payload.name ?? ''),
|
||||
apps,
|
||||
permissions: payload.is_admin
|
||||
? [
|
||||
'fw:dashboard:read',
|
||||
'fw:agents:write',
|
||||
'fw:lists:write',
|
||||
'fw:policies:write',
|
||||
'fw:stats:read',
|
||||
'fw:settings:admin',
|
||||
]
|
||||
: permissions,
|
||||
isAdmin: Boolean(payload.is_admin),
|
||||
}
|
||||
|
||||
const required = permissionForRequest(request.method, request.url)
|
||||
if (
|
||||
required &&
|
||||
!request.authUser.isAdmin &&
|
||||
!hasPermission(request.authUser.permissions, required)
|
||||
) {
|
||||
return reply.code(403).send({
|
||||
error: {
|
||||
code: 'FORBIDDEN',
|
||||
message: `Недостаточно прав: ${required}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
app.addHook('onRequest', async (request, reply) => {
|
||||
if (isPublicPath(request.url)) return
|
||||
|
||||
if (isAgentPath(request.url)) {
|
||||
const auth = request.headers.authorization
|
||||
if (!auth?.startsWith('Bearer ')) {
|
||||
return reply.code(401).send({
|
||||
error: { code: 'UNAUTHORIZED', message: 'Agent token required' },
|
||||
})
|
||||
}
|
||||
const token = auth.slice('Bearer '.length).trim()
|
||||
const agent = repos.getAgentByTokenHash(app.db, hashToken(token))
|
||||
if (!agent || agent.status !== 'approved') {
|
||||
return reply.code(agent?.status === 'pending' ? 403 : 401).send({
|
||||
error: {
|
||||
code: agent?.status === 'pending' ? 'FORBIDDEN' : 'UNAUTHORIZED',
|
||||
message:
|
||||
agent?.status === 'pending'
|
||||
? 'Agent pending approval'
|
||||
: 'Invalid agent token',
|
||||
},
|
||||
})
|
||||
}
|
||||
request.agentId = agent.id
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
declare module 'fastify' {
|
||||
interface FastifyInstance {
|
||||
requireAuth: (
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
) => Promise<void | FastifyReply>
|
||||
}
|
||||
}
|
||||
|
||||
export default fp(authPlugin, { name: 'auth' })
|
||||
export { hashToken }
|
||||
Reference in New Issue
Block a user