feat(api): unify policy handling with default action updates
- 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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user