feat(api, web): implement policy mode management for agents and rules
- Added support for policy modes ('blacklist' and 'whitelist') in agent and policy set management.
- Updated API endpoints to handle policy mode during agent assignment and rule operations.
- Enhanced the web UI to display and manage policy modes for agents and rules, ensuring all assigned sets share a consistent mode.
- Introduced new validation to enforce single policy mode across assigned sets for agents.
- Improved error handling for policy mode conflicts and updated documentation accordingly.
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
-- Mode on policy sets + per-rule enabled
|
||||
|
||||
ALTER TABLE policy_sets ADD COLUMN policy_mode TEXT NOT NULL DEFAULT 'blacklist';
|
||||
|
||||
ALTER TABLE policy_rules ADD COLUMN enabled INTEGER NOT NULL DEFAULT 1;
|
||||
|
||||
-- Backfill set mode from agents that use the set (prefer whitelist if any agent has it)
|
||||
UPDATE policy_sets
|
||||
SET policy_mode = 'whitelist'
|
||||
WHERE id IN (
|
||||
SELECT DISTINCT aps.set_id
|
||||
FROM agent_policy_sets aps
|
||||
INNER JOIN agents a ON a.id = aps.agent_id
|
||||
WHERE a.policy_mode = 'whitelist'
|
||||
);
|
||||
@@ -206,6 +206,7 @@ export function listSetsForAgent(db: Db, agentId: string) {
|
||||
name: policySets.name,
|
||||
description: policySets.description,
|
||||
enabled: policySets.enabled,
|
||||
policyMode: policySets.policyMode,
|
||||
})
|
||||
.from(agentPolicySets)
|
||||
.innerJoin(policySets, eq(agentPolicySets.setId, policySets.id))
|
||||
@@ -214,8 +215,24 @@ export function listSetsForAgent(db: Db, agentId: string) {
|
||||
.all()
|
||||
}
|
||||
|
||||
/** Replace agent↔set assignments; set_ids order = sort. */
|
||||
/** Replace agent↔set assignments; set_ids order = sort. All sets must share policy_mode. */
|
||||
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')
|
||||
}
|
||||
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()
|
||||
setIds.forEach((setId, i) => {
|
||||
db.insert(agentPolicySets)
|
||||
@@ -270,6 +287,7 @@ export function listPolicyRulesForAgent(db: Db, agentId: string) {
|
||||
.all()
|
||||
|
||||
return rules
|
||||
.filter((r) => r.enabled !== 0)
|
||||
.map((r) => ({
|
||||
...r,
|
||||
_setSort: sortBySet.get(r.setId) ?? 0,
|
||||
@@ -289,6 +307,57 @@ export function insertPolicyRule(
|
||||
return getPolicyRule(db, row.id)
|
||||
}
|
||||
|
||||
export function updatePolicyRule(
|
||||
db: Db,
|
||||
id: string,
|
||||
patch: Partial<typeof policyRules.$inferInsert>,
|
||||
) {
|
||||
db.update(policyRules)
|
||||
.set({ ...patch, updatedAt: new Date().toISOString() })
|
||||
.where(eq(policyRules.id, id))
|
||||
.run()
|
||||
return getPolicyRule(db, id)
|
||||
}
|
||||
|
||||
/** Renumber priorities 10, 20, … in given order. */
|
||||
export function reorderPolicyRules(
|
||||
db: Db,
|
||||
setId: string,
|
||||
orderedIds: string[],
|
||||
) {
|
||||
const existing = listPolicyRules(db, setId)
|
||||
const existingIds = new Set(existing.map((r) => r.id))
|
||||
if (
|
||||
orderedIds.length !== existing.length ||
|
||||
orderedIds.some((id) => !existingIds.has(id))
|
||||
) {
|
||||
throw new Error('ordered_ids must list every rule in the set exactly once')
|
||||
}
|
||||
// Temporary priorities to avoid UNIQUE collisions
|
||||
orderedIds.forEach((id, i) => {
|
||||
db.update(policyRules)
|
||||
.set({ priority: 9000 + i, updatedAt: new Date().toISOString() })
|
||||
.where(eq(policyRules.id, id))
|
||||
.run()
|
||||
})
|
||||
orderedIds.forEach((id, i) => {
|
||||
db.update(policyRules)
|
||||
.set({
|
||||
priority: (i + 1) * 10,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(eq(policyRules.id, id))
|
||||
.run()
|
||||
})
|
||||
}
|
||||
|
||||
export function nextRulePriority(db: Db, setId: string): number {
|
||||
const rows = listPolicyRules(db, setId)
|
||||
if (rows.length === 0) return 10
|
||||
const max = Math.max(...rows.map((r) => r.priority))
|
||||
return Math.min(10000, max + 10)
|
||||
}
|
||||
|
||||
export function deletePolicyRule(db: Db, id: string) {
|
||||
db.delete(policyRules).where(eq(policyRules.id, id)).run()
|
||||
}
|
||||
@@ -535,6 +604,9 @@ export const repos = {
|
||||
listPolicyRulesForAgent,
|
||||
getPolicyRule,
|
||||
insertPolicyRule,
|
||||
updatePolicyRule,
|
||||
reorderPolicyRules,
|
||||
nextRulePriority,
|
||||
deletePolicyRule,
|
||||
listHostnameRules,
|
||||
listResolvedForRule,
|
||||
|
||||
@@ -84,6 +84,7 @@ 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
|
||||
createdAt: text('created_at')
|
||||
.notNull()
|
||||
.default(sql`(strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))`),
|
||||
@@ -118,6 +119,7 @@ export const policyRules = sqliteTable(
|
||||
.references(() => policySets.id, { onDelete: 'cascade' }),
|
||||
priority: integer('priority').notNull(),
|
||||
action: text('action').notNull(), // allow | deny
|
||||
enabled: integer('enabled').notNull().default(1),
|
||||
listId: text('list_id').references(() => ipLists.id, { onDelete: 'cascade' }),
|
||||
cidr: text('cidr'),
|
||||
hostname: text('hostname'),
|
||||
|
||||
@@ -61,6 +61,7 @@ export const policyRuleSchema = z.object({
|
||||
set_id: z.string(),
|
||||
priority: z.number().int(),
|
||||
action: policyActionSchema,
|
||||
enabled: z.boolean().optional().default(true),
|
||||
list_id: z.string().nullable().optional(),
|
||||
cidr: z.string().nullable().optional(),
|
||||
hostname: z.string().nullable().optional(),
|
||||
@@ -75,6 +76,7 @@ export const policySetSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().nullable().optional(),
|
||||
enabled: z.boolean(),
|
||||
policy_mode: policyModeSchema,
|
||||
rules_count: z.number().int().optional(),
|
||||
agents_count: z.number().int().optional(),
|
||||
created_at: z.string(),
|
||||
@@ -102,19 +104,22 @@ 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'),
|
||||
})
|
||||
|
||||
export const patchPolicySetBodySchema = z.object({
|
||||
name: z.string().min(1).optional(),
|
||||
description: z.string().nullable().optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
policy_mode: policyModeSchema.optional(),
|
||||
})
|
||||
|
||||
export const createPolicyRuleBodySchema = z
|
||||
.object({
|
||||
set_id: z.string().min(1),
|
||||
priority: z.number().int().min(1).max(10000),
|
||||
priority: z.number().int().min(1).max(10000).optional(),
|
||||
action: policyActionSchema,
|
||||
enabled: z.boolean().optional().default(true),
|
||||
list_id: z.string().nullable().optional(),
|
||||
cidr: z.string().nullable().optional(),
|
||||
hostname: z.string().nullable().optional(),
|
||||
@@ -132,6 +137,17 @@ export const createPolicyRuleBodySchema = z
|
||||
}
|
||||
})
|
||||
|
||||
export const patchPolicyRuleBodySchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
action: policyActionSchema.optional(),
|
||||
comment: z.string().nullable().optional(),
|
||||
priority: z.number().int().min(1).max(10000).optional(),
|
||||
})
|
||||
|
||||
export const reorderPolicyRulesBodySchema = z.object({
|
||||
ordered_ids: z.array(z.string()).min(1),
|
||||
})
|
||||
|
||||
export const putAgentPolicySetsBodySchema = z.object({
|
||||
set_ids: z.array(z.string()),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user