feat(api): unify policy handling with default action updates
Build and Push EvoFirewall Docker Image / build-and-push (push) Successful in 1m53s
Build and Push EvoFirewall Docker Image / create-release (push) Skipped

- Updated `evofw-firewall.sh` and related scripts to replace `policy_mode` with `default_action`, enhancing clarity and consistency in policy management.
- Adjusted agent routes and evaluation logic to accommodate the new default action structure, ensuring backward compatibility with legacy modes.
- Enhanced tests to validate the new default action behavior and its integration within the agent policy framework.
- Refactored related components in the web interface to align with the updated policy handling, improving user experience and reducing confusion around policy modes.
This commit is contained in:
Denozordec
2026-07-23 10:52:28 +07:00
parent a6eb21a10d
commit 1f7273f38d
30 changed files with 1469 additions and 621 deletions
+91 -49
View File
@@ -23,7 +23,7 @@ import {
deleteListEntry,
mapListDetail,
} from '../services/lists/entries.js'
import { evaluateAgentPolicy } from '../services/policy/evaluate.js'
import { evaluateAgentPolicy, truncateCidrs } from '../services/policy/evaluate.js'
import {
resolveAndStoreHostnameRule,
resolveHostnameToCidrs,
@@ -41,6 +41,8 @@ function mapAgent(
a: NonNullable<ReturnType<typeof repos.getAgent>>,
opts?: { installCurl?: string | null; installLinkId?: string | null },
) {
const defaultAction =
a.defaultAction === 'drop' ? ('drop' as const) : ('accept' as const)
return {
id: a.id,
name: a.name,
@@ -48,7 +50,8 @@ function mapAgent(
platform: a.platform,
token_prefix: a.tokenPrefix,
status: a.status,
policy_mode: a.policyMode,
default_action: defaultAction,
policy_mode: defaultAction === 'drop' ? ('whitelist' as const) : ('blacklist' as const),
policy_generation: a.policyGeneration,
last_seen_at: a.lastSeenAt,
last_seen_ip: a.lastSeenIp,
@@ -77,8 +80,6 @@ function mapPolicySet(
name: s.name,
description: s.description,
enabled: s.enabled === 1,
policy_mode:
s.policyMode === 'whitelist' ? ('whitelist' as const) : ('blacklist' as const),
rules_count: repos.countRulesInSet(db, s.id),
agents_count: repos.countAgentsForSet(db, s.id),
created_at: s.createdAt,
@@ -185,7 +186,7 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
tokenPrefix: inviteToken.slice(0, 12),
tokenHash: hashToken(inviteToken),
status: 'invited',
policyMode: 'blacklist',
defaultAction: 'accept',
policyGeneration: 1,
clientVersion: null,
settingsJson: '{}',
@@ -252,16 +253,45 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
return mapAgent(a)
})
app.get<{ Params: { id: string } }>('/agents/:id/preview', async (req) => {
app.get<{
Params: { id: string }
Querystring: { limit_cidrs?: string }
}>('/agents/:id/preview', async (req) => {
const a = repos.getAgent(app.db, req.params.id)
if (!a) throw new AppError('NOT_FOUND', 'Agent not found', 404)
const policy = evaluateAgentPolicy(app.db, a.id)
const limitRaw = Number(req.query.limit_cidrs ?? '50')
const limit = Number.isFinite(limitRaw)
? Math.min(Math.max(0, Math.floor(limitRaw)), 5000)
: 50
return {
...policy,
deny_cidrs: policy.denyCidrs,
allow_cidrs: policy.allowCidrs,
policy_mode: policy.policyMode,
default_action: policy.defaultAction,
hash: policy.hash,
generation: policy.generation,
sync_interval_sec: policy.syncIntervalSec,
apply_version: policy.applyVersion,
summary: {
sets: policy.summary.sets,
rules_deny: policy.summary.rulesDeny,
rules_allow: policy.summary.rulesAllow,
cidrs_deny: policy.summary.cidrsDeny,
cidrs_allow: policy.summary.cidrsAllow,
overrides: policy.summary.overrides,
conflicts_dropped: policy.summary.conflictsDropped,
},
chain: policy.chain.map((s) => ({
set_id: s.setId,
set_name: s.setName,
rule_id: s.ruleId,
action: s.action,
source_kind: s.sourceKind,
source_label: s.sourceLabel,
cidr_count: s.cidrCount,
})),
deny_cidrs: truncateCidrs(policy.denyCidrs, limit),
allow_cidrs: truncateCidrs(policy.allowCidrs, limit),
deny_cidrs_total: policy.denyCidrs.length,
allow_cidrs_total: policy.allowCidrs.length,
}
})
@@ -269,14 +299,15 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
const body = patchAgentBodySchema.parse(req.body)
const a = repos.getAgent(app.db, req.params.id)
if (!a) throw new AppError('NOT_FOUND', 'Agent not found', 404)
const updated = repos.updateAgent(app.db, a.id, {
const nextDefault = body.default_action
const updated = repos.updateAgent(app.db, a.id, {
name: body.name,
policyMode: body.policy_mode,
defaultAction: nextDefault,
settingsJson: body.settings
? JSON.stringify(body.settings)
: undefined,
policyGeneration:
body.policy_mode && body.policy_mode !== a.policyMode
nextDefault && nextDefault !== a.defaultAction
? a.policyGeneration + 1
: a.policyGeneration,
})
@@ -287,7 +318,7 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
summary: `Обновлён агент: ${updated!.name}`,
details: {
agent_id: a.id,
policy_mode: body.policy_mode,
default_action: nextDefault,
name: body.name,
},
})
@@ -624,7 +655,7 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
name: body.name.trim(),
description: body.description ?? null,
enabled: body.enabled === false ? 0 : 1,
policyMode: body.policy_mode === 'whitelist' ? 'whitelist' : 'blacklist',
policyMode: 'blacklist',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
})
@@ -633,7 +664,7 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
targetType: 'app_resource',
targetId: row!.id,
summary: `Создан набор политик: ${row!.name}`,
details: { set_id: row!.id, policy_mode: row!.policyMode },
details: { set_id: row!.id },
})
return mapPolicySet(row!, app.db)
})
@@ -646,37 +677,10 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
name: body.name?.trim(),
description: body.description,
enabled: body.enabled === undefined ? undefined : body.enabled ? 1 : 0,
policyMode: body.policy_mode,
})
if (body.enabled !== undefined || body.policy_mode !== undefined) {
if (body.enabled !== undefined) {
repos.bumpAgentsForSet(app.db, s.id)
}
// Sync agent.policy_mode cache when set mode changes
if (body.policy_mode) {
for (const agentId of repos.listAgentIdsForSet(app.db, s.id)) {
try {
const sets = repos.listSetsForAgent(app.db, agentId)
const modes = new Set(
sets
.filter((x) => x.enabled === 1)
.map((x) =>
x.policyMode === 'whitelist' ? 'whitelist' : 'blacklist',
),
)
if (modes.size > 1) {
throw new AppError(
'VALIDATION_ERROR',
'агент имеет наборы с разными режимами — выровняйте mode',
400,
)
}
const mode = [...modes][0] ?? body.policy_mode
repos.updateAgent(app.db, agentId, { policyMode: mode })
} catch (err) {
if (err instanceof AppError) throw err
}
}
}
auditMutation(app, config, req, {
action: 'policy_set.update',
targetType: 'app_resource',
@@ -685,7 +689,6 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
details: {
set_id: s.id,
enabled: body.enabled,
policy_mode: body.policy_mode,
name: body.name,
},
})
@@ -765,8 +768,6 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
name: s.name,
description: s.description,
enabled: s.enabled === 1,
policy_mode:
s.policyMode === 'whitelist' ? 'whitelist' : 'blacklist',
})),
}
},
@@ -784,8 +785,6 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
name: s.name,
description: s.description,
enabled: s.enabled === 1,
policy_mode:
s.policyMode === 'whitelist' ? 'whitelist' : 'blacklist',
})),
}
},
@@ -981,6 +980,49 @@ export const controlRoutes: FastifyPluginAsync<{ config: AppConfig }> = async (
})),
}))
/** Proxy EvoBGP communities for UI autocomplete. */
app.get('/integrations/evobgp/communities', async () => {
const apiUrl = repos.getSetting(app.db, 'evobgp_api_url')
const token = repos.getSetting(app.db, 'evobgp_api_token')
if (!apiUrl || !token) {
throw new AppError(
'VALIDATION_ERROR',
'Настройте evobgp_api_url и evobgp_api_token',
400,
)
}
const base = apiUrl.replace(/\/$/, '')
const res = await fetch(`${base}/v1/communities?limit=200`, {
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/json',
},
signal: AbortSignal.timeout(20_000),
})
if (!res.ok) {
throw new AppError(
'UPSTREAM_ERROR',
`EvoBGP communities HTTP ${res.status}`,
502,
)
}
const data = (await res.json()) as {
items?: {
id?: string
community?: string
title?: string | null
}[]
}
const items = (data.items ?? [])
.filter((x) => x.id && x.community)
.map((x) => ({
id: x.id!,
community: x.community!,
title: x.title ?? null,
}))
return { items }
})
// Settings
app.get('/settings', async () => {
const rows = repos.listSettings(app.db)