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:
@@ -0,0 +1,64 @@
|
||||
-- Replace exclusive blacklist/whitelist with default_action (accept|drop).
|
||||
-- Unified kernel chain: deny → allow → default_action.
|
||||
|
||||
PRAGMA foreign_keys = OFF;
|
||||
|
||||
CREATE TABLE agents_v3 (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
hostname TEXT,
|
||||
platform TEXT NOT NULL DEFAULT 'linux',
|
||||
token_prefix TEXT NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
default_action TEXT NOT NULL DEFAULT 'accept',
|
||||
policy_generation INTEGER NOT NULL DEFAULT 1,
|
||||
last_seen_at TEXT,
|
||||
last_seen_ip TEXT,
|
||||
last_apply_at TEXT,
|
||||
last_apply_status TEXT,
|
||||
last_apply_error TEXT,
|
||||
last_apply_prefix_count INTEGER DEFAULT 0,
|
||||
last_apply_packets_dropped INTEGER NOT NULL DEFAULT 0,
|
||||
last_apply_packets_accepted INTEGER NOT NULL DEFAULT 0,
|
||||
last_apply_kernel_method TEXT,
|
||||
client_version TEXT,
|
||||
settings_json TEXT NOT NULL DEFAULT '{}',
|
||||
created_by_user_id TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
approved_at TEXT,
|
||||
revoked_at TEXT,
|
||||
CHECK (status IN ('invited', 'pending', 'approved', 'revoked')),
|
||||
CHECK (platform IN ('linux', 'mikrotik')),
|
||||
CHECK (default_action IN ('accept', 'drop')),
|
||||
CHECK (length(trim(name)) > 0)
|
||||
);
|
||||
|
||||
INSERT INTO agents_v3 (
|
||||
id, name, hostname, platform, token_prefix, token_hash, status, default_action,
|
||||
policy_generation, last_seen_at, last_seen_ip, last_apply_at, last_apply_status,
|
||||
last_apply_error, last_apply_prefix_count, last_apply_packets_dropped,
|
||||
last_apply_packets_accepted, last_apply_kernel_method, client_version,
|
||||
settings_json, created_by_user_id, created_at, approved_at, revoked_at
|
||||
)
|
||||
SELECT
|
||||
id, name, hostname, platform, token_prefix, token_hash, status,
|
||||
CASE
|
||||
WHEN policy_mode = 'whitelist' THEN 'drop'
|
||||
WHEN policy_mode = 'drop' THEN 'drop'
|
||||
WHEN policy_mode = 'accept' THEN 'accept'
|
||||
ELSE 'accept'
|
||||
END,
|
||||
policy_generation, last_seen_at, last_seen_ip, last_apply_at, last_apply_status,
|
||||
last_apply_error, last_apply_prefix_count, last_apply_packets_dropped,
|
||||
last_apply_packets_accepted, last_apply_kernel_method, client_version,
|
||||
settings_json, created_by_user_id, created_at, approved_at, revoked_at
|
||||
FROM agents;
|
||||
|
||||
DROP TABLE agents;
|
||||
ALTER TABLE agents_v3 RENAME TO agents;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_agents_token_hash ON agents (token_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_agents_status ON agents (status);
|
||||
|
||||
PRAGMA foreign_keys = ON;
|
||||
@@ -215,22 +215,12 @@ export function listSetsForAgent(db: Db, agentId: string) {
|
||||
.all()
|
||||
}
|
||||
|
||||
/** Replace agent↔set assignments; set_ids order = sort. All sets must share policy_mode. */
|
||||
/** Replace agent↔set assignments; set_ids order = sort. */
|
||||
export function setAgentPolicySets(db: Db, agentId: string, setIds: string[]) {
|
||||
if (setIds.length > 0) {
|
||||
const modes = new Set<string>()
|
||||
for (const setId of setIds) {
|
||||
const s = getPolicySet(db, setId)
|
||||
if (!s) throw new Error(`policy set not found: ${setId}`)
|
||||
modes.add(s.policyMode === 'whitelist' ? 'whitelist' : 'blacklist')
|
||||
for (const setId of setIds) {
|
||||
if (!getPolicySet(db, setId)) {
|
||||
throw new Error(`policy set not found: ${setId}`)
|
||||
}
|
||||
if (modes.size > 1) {
|
||||
throw new Error(
|
||||
'все наборы агента должны иметь один режим (blacklist или whitelist)',
|
||||
)
|
||||
}
|
||||
const mode = [...modes][0] ?? 'blacklist'
|
||||
updateAgent(db, agentId, { policyMode: mode })
|
||||
}
|
||||
|
||||
db.delete(agentPolicySets).where(eq(agentPolicySets.agentId, agentId)).run()
|
||||
@@ -498,7 +488,7 @@ export function cloneRulesFrom(
|
||||
}
|
||||
|
||||
updateAgent(db, targetAgentId, {
|
||||
policyMode: source.policyMode,
|
||||
defaultAction: source.defaultAction,
|
||||
policyGeneration: (target.policyGeneration ?? 1) + 1,
|
||||
})
|
||||
return getAgent(db, targetAgentId)
|
||||
|
||||
@@ -19,7 +19,8 @@ export const agents = sqliteTable(
|
||||
tokenPrefix: text('token_prefix').notNull(),
|
||||
tokenHash: text('token_hash').notNull(),
|
||||
status: text('status').notNull().default('pending'), // invited | pending | approved | revoked
|
||||
policyMode: text('policy_mode').notNull().default('blacklist'), // blacklist | whitelist
|
||||
/** Packet default when not in deny/allow sets: accept | drop */
|
||||
defaultAction: text('default_action').notNull().default('accept'),
|
||||
policyGeneration: integer('policy_generation').notNull().default(1),
|
||||
lastSeenAt: text('last_seen_at'),
|
||||
lastSeenIp: text('last_seen_ip'),
|
||||
@@ -84,7 +85,8 @@ export const policySets = sqliteTable('policy_sets', {
|
||||
name: text('name').notNull(),
|
||||
description: text('description'),
|
||||
enabled: integer('enabled').notNull().default(1),
|
||||
policyMode: text('policy_mode').notNull().default('blacklist'), // blacklist | whitelist
|
||||
/** Legacy unused; sets no longer carry exclusive mode. */
|
||||
policyMode: text('policy_mode').notNull().default('blacklist'),
|
||||
createdAt: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
||||
|
||||
@@ -7,7 +7,13 @@ export const agentStatusSchema = z.enum([
|
||||
'approved',
|
||||
'revoked',
|
||||
])
|
||||
|
||||
/** Packet default when CIDR is in neither deny nor allow set. */
|
||||
export const defaultActionSchema = z.enum(['accept', 'drop'])
|
||||
|
||||
/** @deprecated Use defaultActionSchema. Kept for API input compat. */
|
||||
export const policyModeSchema = z.enum(['blacklist', 'whitelist'])
|
||||
|
||||
export const policyActionSchema = z.enum(['allow', 'deny'])
|
||||
export const ipListTypeSchema = z.enum([
|
||||
'static',
|
||||
@@ -16,6 +22,21 @@ export const ipListTypeSchema = z.enum([
|
||||
'evobgp_community',
|
||||
])
|
||||
|
||||
/** Map legacy blacklist/whitelist → default_action. */
|
||||
export function defaultActionFromLegacyMode(
|
||||
mode: string | null | undefined,
|
||||
): 'accept' | 'drop' {
|
||||
if (mode === 'whitelist' || mode === 'drop') return 'drop'
|
||||
return 'accept'
|
||||
}
|
||||
|
||||
/** Optional mirror for old agent binaries. */
|
||||
export function legacyModeFromDefaultAction(
|
||||
action: 'accept' | 'drop',
|
||||
): 'blacklist' | 'whitelist' {
|
||||
return action === 'drop' ? 'whitelist' : 'blacklist'
|
||||
}
|
||||
|
||||
export const agentSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
@@ -23,7 +44,9 @@ export const agentSchema = z.object({
|
||||
platform: agentPlatformSchema,
|
||||
token_prefix: z.string(),
|
||||
status: agentStatusSchema,
|
||||
policy_mode: policyModeSchema,
|
||||
default_action: defaultActionSchema,
|
||||
/** @deprecated mirror of default_action for older clients */
|
||||
policy_mode: policyModeSchema.optional(),
|
||||
policy_generation: z.number().int(),
|
||||
last_seen_at: z.string().nullable().optional(),
|
||||
last_seen_ip: z.string().nullable().optional(),
|
||||
@@ -76,7 +99,8 @@ export const policySetSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
enabled: z.boolean(),
|
||||
policy_mode: policyModeSchema,
|
||||
/** @deprecated ignored — sets have no exclusive mode */
|
||||
policy_mode: policyModeSchema.optional(),
|
||||
rules_count: z.number().int().optional(),
|
||||
agents_count: z.number().int().optional(),
|
||||
created_at: z.string(),
|
||||
@@ -104,13 +128,15 @@ export const createPolicySetBodySchema = z.object({
|
||||
name: z.string().min(1),
|
||||
description: z.string().nullable().optional(),
|
||||
enabled: z.boolean().optional().default(true),
|
||||
policy_mode: policyModeSchema.optional().default('blacklist'),
|
||||
/** @deprecated ignored */
|
||||
policy_mode: policyModeSchema.optional(),
|
||||
})
|
||||
|
||||
export const patchPolicySetBodySchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
/** @deprecated ignored */
|
||||
policy_mode: policyModeSchema.optional(),
|
||||
})
|
||||
|
||||
@@ -158,11 +184,26 @@ export const createOverrideBodySchema = z.object({
|
||||
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 patchAgentBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1).optional(),
|
||||
default_action: defaultActionSchema.optional(),
|
||||
/** @deprecated use default_action */
|
||||
policy_mode: policyModeSchema.optional(),
|
||||
settings: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
.transform((v) => {
|
||||
const default_action =
|
||||
v.default_action ??
|
||||
(v.policy_mode !== undefined
|
||||
? defaultActionFromLegacyMode(v.policy_mode)
|
||||
: undefined)
|
||||
return {
|
||||
name: v.name,
|
||||
default_action,
|
||||
settings: v.settings,
|
||||
}
|
||||
})
|
||||
|
||||
export const cloneFromBodySchema = z.object({
|
||||
include_overrides: z.boolean().optional().default(false),
|
||||
@@ -190,12 +231,47 @@ export const applyReportBodySchema = z.object({
|
||||
export const agentPolicySchema = z.object({
|
||||
generation: z.number().int(),
|
||||
hash: z.string(),
|
||||
policy_mode: policyModeSchema,
|
||||
apply_version: z.number().int(),
|
||||
default_action: defaultActionSchema,
|
||||
/** @deprecated mirror for old agents */
|
||||
policy_mode: policyModeSchema.optional(),
|
||||
deny_cidrs: z.array(z.string()),
|
||||
allow_cidrs: z.array(z.string()),
|
||||
sync_interval_sec: z.number().int(),
|
||||
})
|
||||
|
||||
export const agentPolicyPreviewSchema = z.object({
|
||||
default_action: defaultActionSchema,
|
||||
hash: z.string(),
|
||||
generation: z.number().int(),
|
||||
sync_interval_sec: z.number().int(),
|
||||
apply_version: z.literal(2),
|
||||
summary: z.object({
|
||||
sets: z.number().int(),
|
||||
rules_deny: z.number().int(),
|
||||
rules_allow: z.number().int(),
|
||||
cidrs_deny: z.number().int(),
|
||||
cidrs_allow: z.number().int(),
|
||||
overrides: z.number().int(),
|
||||
conflicts_dropped: z.number().int(),
|
||||
}),
|
||||
chain: z.array(
|
||||
z.object({
|
||||
set_id: z.string().nullable(),
|
||||
set_name: z.string().nullable(),
|
||||
rule_id: z.string().nullable(),
|
||||
action: policyActionSchema,
|
||||
source_kind: z.enum(['list', 'cidr', 'hostname', 'override']),
|
||||
source_label: z.string(),
|
||||
cidr_count: z.number().int(),
|
||||
}),
|
||||
),
|
||||
deny_cidrs: z.array(z.string()),
|
||||
allow_cidrs: z.array(z.string()),
|
||||
deny_cidrs_total: z.number().int(),
|
||||
allow_cidrs_total: z.number().int(),
|
||||
})
|
||||
|
||||
export const dashboardStatsSchema = z.object({
|
||||
agents_total: z.number().int(),
|
||||
agents_approved: z.number().int(),
|
||||
@@ -235,11 +311,20 @@ export const installLinkSchema = z.object({
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export const evobgpCommunitySchema = z.object({
|
||||
id: z.string(),
|
||||
community: z.string(),
|
||||
title: z.string().nullable().optional(),
|
||||
})
|
||||
|
||||
export type Agent = z.infer<typeof agentSchema>
|
||||
export type IpList = z.infer<typeof ipListSchema>
|
||||
export type PolicyRule = z.infer<typeof policyRuleSchema>
|
||||
export type PolicySet = z.infer<typeof policySetSchema>
|
||||
export type IpOverride = z.infer<typeof ipOverrideSchema>
|
||||
export type AgentPolicy = z.infer<typeof agentPolicySchema>
|
||||
export type AgentPolicyPreview = z.infer<typeof agentPolicyPreviewSchema>
|
||||
export type DashboardStats = z.infer<typeof dashboardStatsSchema>
|
||||
export type InstallLink = z.infer<typeof installLinkSchema>
|
||||
export type EvobgpCommunity = z.infer<typeof evobgpCommunitySchema>
|
||||
export type DefaultAction = z.infer<typeof defaultActionSchema>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user