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,147 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const agentPlatformSchema = z.enum(['linux', 'mikrotik'])
|
||||
export const agentStatusSchema = z.enum(['pending', 'approved', 'revoked'])
|
||||
export const policyModeSchema = z.enum(['blacklist', 'whitelist'])
|
||||
export const policyActionSchema = z.enum(['allow', 'deny'])
|
||||
export const ipListTypeSchema = z.enum([
|
||||
'static',
|
||||
'json_url',
|
||||
'domains',
|
||||
'evobgp_community',
|
||||
])
|
||||
|
||||
export const agentSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
hostname: z.string().nullable().optional(),
|
||||
platform: agentPlatformSchema,
|
||||
token_prefix: z.string(),
|
||||
status: agentStatusSchema,
|
||||
policy_mode: policyModeSchema,
|
||||
policy_generation: z.number().int(),
|
||||
last_seen_at: z.string().nullable().optional(),
|
||||
last_seen_ip: z.string().nullable().optional(),
|
||||
last_apply_at: z.string().nullable().optional(),
|
||||
last_apply_status: z.string().nullable().optional(),
|
||||
last_apply_error: z.string().nullable().optional(),
|
||||
last_apply_prefix_count: z.number().int().nullable().optional(),
|
||||
last_apply_packets_dropped: z.number().int().optional(),
|
||||
last_apply_packets_accepted: z.number().int().optional(),
|
||||
last_apply_kernel_method: z.string().nullable().optional(),
|
||||
client_version: z.string().nullable().optional(),
|
||||
settings_json: z.string().optional(),
|
||||
created_at: z.string(),
|
||||
approved_at: z.string().nullable().optional(),
|
||||
revoked_at: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export const ipListSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: ipListTypeSchema,
|
||||
config_json: z.string(),
|
||||
content_hash: z.string().nullable().optional(),
|
||||
refreshed_at: z.string().nullable().optional(),
|
||||
last_error: z.string().nullable().optional(),
|
||||
entry_count: z.number().int().optional(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
|
||||
export const policyRuleSchema = z.object({
|
||||
id: z.string(),
|
||||
agent_id: z.string().nullable().optional(),
|
||||
priority: z.number().int(),
|
||||
action: policyActionSchema,
|
||||
list_id: z.string().nullable().optional(),
|
||||
cidr: z.string().nullable().optional(),
|
||||
comment: z.string().nullable().optional(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
})
|
||||
|
||||
export const ipOverrideSchema = z.object({
|
||||
id: z.string(),
|
||||
agent_id: z.string(),
|
||||
cidr: z.string(),
|
||||
action: policyActionSchema,
|
||||
comment: z.string().nullable().optional(),
|
||||
created_at: z.string(),
|
||||
})
|
||||
|
||||
export const createIpListBodySchema = z.object({
|
||||
name: z.string().min(1),
|
||||
type: ipListTypeSchema,
|
||||
config: z.record(z.string(), z.unknown()).optional(),
|
||||
entries: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const createPolicyRuleBodySchema = z.object({
|
||||
agent_id: z.string().nullable().optional(),
|
||||
priority: z.number().int().min(1).max(10000),
|
||||
action: policyActionSchema,
|
||||
list_id: z.string().nullable().optional(),
|
||||
cidr: z.string().nullable().optional(),
|
||||
comment: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export const createOverrideBodySchema = z.object({
|
||||
cidr: z.string().min(1),
|
||||
action: policyActionSchema,
|
||||
comment: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export const patchAgentBodySchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
policy_mode: policyModeSchema.optional(),
|
||||
settings: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export const cloneFromBodySchema = z.object({
|
||||
include_overrides: z.boolean().optional().default(false),
|
||||
})
|
||||
|
||||
export const enrollBodySchema = z.object({
|
||||
name: z.string().min(1),
|
||||
hostname: z.string().optional(),
|
||||
platform: agentPlatformSchema.optional().default('linux'),
|
||||
token: z.string().min(16),
|
||||
client_version: z.string().optional(),
|
||||
})
|
||||
|
||||
export const applyReportBodySchema = z.object({
|
||||
status: z.string(),
|
||||
prefix_count: z.number().int().optional(),
|
||||
packets_dropped: z.number().int().optional(),
|
||||
packets_accepted: z.number().int().optional(),
|
||||
kernel_method: z.string().optional(),
|
||||
error: z.string().optional(),
|
||||
source: z.string().optional(),
|
||||
})
|
||||
|
||||
export const agentPolicySchema = z.object({
|
||||
generation: z.number().int(),
|
||||
hash: z.string(),
|
||||
policy_mode: policyModeSchema,
|
||||
deny_cidrs: z.array(z.string()),
|
||||
allow_cidrs: z.array(z.string()),
|
||||
sync_interval_sec: z.number().int(),
|
||||
})
|
||||
|
||||
export const dashboardStatsSchema = z.object({
|
||||
agents_total: z.number().int(),
|
||||
agents_approved: z.number().int(),
|
||||
agents_online: z.number().int(),
|
||||
agents_pending: z.number().int(),
|
||||
packets_dropped: z.number().int(),
|
||||
packets_accepted: z.number().int(),
|
||||
lists_total: z.number().int(),
|
||||
})
|
||||
|
||||
export type Agent = z.infer<typeof agentSchema>
|
||||
export type IpList = z.infer<typeof ipListSchema>
|
||||
export type PolicyRule = z.infer<typeof policyRuleSchema>
|
||||
export type IpOverride = z.infer<typeof ipOverrideSchema>
|
||||
export type AgentPolicy = z.infer<typeof agentPolicySchema>
|
||||
export type DashboardStats = z.infer<typeof dashboardStatsSchema>
|
||||
@@ -1 +1,2 @@
|
||||
export {}
|
||||
export * from './contracts.js'
|
||||
export * from './permissions.js'
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
export type PermissionAction = 'read' | 'write' | 'admin'
|
||||
|
||||
/** Hierarchy: admin ⊃ write ⊃ read within the same section. */
|
||||
export function hasPermission(
|
||||
granted: readonly string[],
|
||||
required: string,
|
||||
): boolean {
|
||||
if (granted.includes(required)) return true
|
||||
const parts = required.split(':')
|
||||
if (parts.length !== 3) return false
|
||||
const [app, section, action] = parts
|
||||
if (action === 'read') {
|
||||
return (
|
||||
granted.includes(`${app}:${section}:write`) ||
|
||||
granted.includes(`${app}:${section}:admin`)
|
||||
)
|
||||
}
|
||||
if (action === 'write') {
|
||||
return granted.includes(`${app}:${section}:admin`)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export function permissionForRequest(
|
||||
method: string,
|
||||
url: string,
|
||||
): string | null {
|
||||
const path = url.split('?')[0] ?? url
|
||||
const m = method.toUpperCase()
|
||||
const write = m !== 'GET' && m !== 'HEAD' && m !== 'OPTIONS'
|
||||
|
||||
if (path.startsWith('/api/v1/agents')) {
|
||||
return write ? 'fw:agents:write' : 'fw:agents:read'
|
||||
}
|
||||
if (path.startsWith('/api/v1/lists')) {
|
||||
return write ? 'fw:lists:write' : 'fw:lists:read'
|
||||
}
|
||||
if (path.startsWith('/api/v1/rules') || path.startsWith('/api/v1/policies')) {
|
||||
return write ? 'fw:policies:write' : 'fw:policies:read'
|
||||
}
|
||||
if (path.startsWith('/api/v1/stats') || path.startsWith('/api/v1/dashboard')) {
|
||||
return 'fw:stats:read'
|
||||
}
|
||||
if (path.startsWith('/api/v1/settings') || path.startsWith('/api/v1/install-context')) {
|
||||
return write ? 'fw:settings:admin' : 'fw:settings:read'
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export type AuthUser = {
|
||||
id: string
|
||||
email: string
|
||||
name: string
|
||||
apps: string[]
|
||||
permissions: string[]
|
||||
isAdmin: boolean
|
||||
}
|
||||
Reference in New Issue
Block a user