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:
@@ -1,31 +1,20 @@
|
||||
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { toast } from 'sonner'
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { useRef, useState } from 'react'
|
||||
import {
|
||||
BanIcon,
|
||||
CheckCircle2Icon,
|
||||
CircleAlertIcon,
|
||||
ClockIcon,
|
||||
Copy,
|
||||
CpuIcon,
|
||||
CopyPlusIcon,
|
||||
ShieldOffIcon,
|
||||
CpuIcon,
|
||||
MoreHorizontalIcon,
|
||||
ShieldPlusIcon,
|
||||
TerminalIcon,
|
||||
} from 'lucide-react'
|
||||
import {
|
||||
DetailPanel,
|
||||
PageShell,
|
||||
QuickActionGrid,
|
||||
} from '@/components/reui-kit'
|
||||
import {
|
||||
Frame,
|
||||
FrameDescription,
|
||||
FrameHeader,
|
||||
FramePanel,
|
||||
FrameTitle,
|
||||
} from '@/components/reui/frame'
|
||||
import { DetailPanel, PageShell } from '@/components/reui-kit'
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
@@ -36,21 +25,34 @@ import {
|
||||
AgentPlatformIcon,
|
||||
platformLabel,
|
||||
} from '@/components/agents/agent-platform-icon'
|
||||
import { AgentLifecycleTimeline } from '@/components/agents/agent-lifecycle-timeline'
|
||||
import { AgentPolicySetsSortable } from '@/components/agents/agent-policy-sets-sortable'
|
||||
import { AgentPolicyTrace } from '@/components/agents/agent-policy-trace'
|
||||
import { AgentFactsPanel } from '@/components/agents/agent-facts-panel'
|
||||
import { AgentEffectiveCidrs } from '@/components/agents/agent-effective-cidrs'
|
||||
import {
|
||||
AgentCloneSetsSheet,
|
||||
AgentOverrideSheet,
|
||||
} from '@/components/agents/agent-settings-sheets'
|
||||
import { agentQueryOptions } from '@/queries'
|
||||
import {
|
||||
agentPreviewQueryOptions,
|
||||
agentQueryOptions,
|
||||
} from '@/queries'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard'
|
||||
import { Button } from '@evofw/ui/components/button'
|
||||
import { Skeleton } from '@evofw/ui/components/skeleton'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@evofw/ui/components/dropdown-menu'
|
||||
|
||||
/**
|
||||
* Agent detail — Solutions Agents DNA.
|
||||
* Preview: https://reui.io/preview/base/solution-agents-3 · stats-12 · sheet-8 · c-sortable-5
|
||||
* Agent detail — SA3 layout DNA (Header + Trace 2/3 + Facts 1/3).
|
||||
* Preview: https://reui.io/preview/base/solution-agents-3
|
||||
* · https://reui.io/preview/base/stats-12
|
||||
* Docs: https://reui.io/blocks/solutions/agents
|
||||
*/
|
||||
|
||||
export const Route = createFileRoute('/_auth/agents/$id')({
|
||||
@@ -58,6 +60,7 @@ export const Route = createFileRoute('/_auth/agents/$id')({
|
||||
const agent = await queryClient.ensureQueryData(
|
||||
agentQueryOptions(params.id),
|
||||
)
|
||||
void queryClient.ensureQueryData(agentPreviewQueryOptions(params.id))
|
||||
return { breadcrumb: agent.name }
|
||||
},
|
||||
component: AgentDetailPage,
|
||||
@@ -68,6 +71,7 @@ function AgentDetailPage() {
|
||||
const qc = useQueryClient()
|
||||
const { copyToClipboard } = useCopyToClipboard()
|
||||
const agentQ = useQuery(agentQueryOptions(id))
|
||||
const previewQ = useQuery(agentPreviewQueryOptions(id))
|
||||
const installRef = useRef<HTMLDivElement>(null)
|
||||
const [overrideOpen, setOverrideOpen] = useState(false)
|
||||
const [cloneOpen, setCloneOpen] = useState(false)
|
||||
@@ -94,68 +98,6 @@ function AgentDetailPage() {
|
||||
|
||||
const a = agentQ.data
|
||||
|
||||
const quickActions = useMemo(() => {
|
||||
if (!a) return []
|
||||
const actions = [
|
||||
{
|
||||
id: 'override',
|
||||
title: 'IP override',
|
||||
description: 'Allow/deny поверх политики',
|
||||
icon: <ShieldPlusIcon aria-hidden />,
|
||||
iconClassName: 'text-warning [&_svg]:text-current',
|
||||
badgeLabel: 'Открыть',
|
||||
onSelect: () => setOverrideOpen(true),
|
||||
},
|
||||
{
|
||||
id: 'clone',
|
||||
title: 'Копировать наборы',
|
||||
description: 'С другого агента + overrides',
|
||||
icon: <CopyPlusIcon aria-hidden />,
|
||||
iconClassName: 'text-info [&_svg]:text-current',
|
||||
badgeLabel: 'Открыть',
|
||||
onSelect: () => setCloneOpen(true),
|
||||
},
|
||||
{
|
||||
id: 'install',
|
||||
title: 'Install curl',
|
||||
description: a.install_curl ? 'Скопировать one-liner' : 'Недоступен',
|
||||
icon: <TerminalIcon aria-hidden />,
|
||||
iconClassName: 'text-primary [&_svg]:text-current',
|
||||
badgeLabel: 'Копировать',
|
||||
onSelect: () => {
|
||||
if (a.install_curl) {
|
||||
copyToClipboard(a.install_curl)
|
||||
toast.success('Скопировано')
|
||||
}
|
||||
installRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
},
|
||||
},
|
||||
]
|
||||
if (a.status === 'pending') {
|
||||
actions.push({
|
||||
id: 'approve',
|
||||
title: 'Approve',
|
||||
description: 'Выдать политику агенту',
|
||||
icon: <CheckCircle2Icon aria-hidden />,
|
||||
iconClassName: 'text-success [&_svg]:text-current',
|
||||
badgeLabel: 'Выполнить',
|
||||
onSelect: () => approve.mutate(),
|
||||
})
|
||||
}
|
||||
if (a.status === 'approved') {
|
||||
actions.push({
|
||||
id: 'revoke',
|
||||
title: 'Revoke',
|
||||
description: 'Отозвать доступ агента',
|
||||
icon: <ShieldOffIcon aria-hidden />,
|
||||
iconClassName: 'text-destructive [&_svg]:text-current',
|
||||
badgeLabel: 'Выполнить',
|
||||
onSelect: () => revoke.mutate(),
|
||||
})
|
||||
}
|
||||
return actions
|
||||
}, [a, approve, copyToClipboard, revoke])
|
||||
|
||||
if (agentQ.isLoading || !a) {
|
||||
return (
|
||||
<PageShell>
|
||||
@@ -171,6 +113,7 @@ function AgentDetailPage() {
|
||||
a.hostname,
|
||||
platformLabel(a.platform),
|
||||
`gen ${a.policy_generation}`,
|
||||
a.default_action === 'drop' ? 'default Drop' : 'default Accept',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · ')
|
||||
@@ -217,13 +160,44 @@ function AgentDetailPage() {
|
||||
Install
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
render={<Link to="/agents" />}
|
||||
>
|
||||
К списку
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
render={
|
||||
<Button variant="outline" size="icon-sm" aria-label="Ещё" />
|
||||
}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => setOverrideOpen(true)}>
|
||||
<ShieldPlusIcon className="size-4" />
|
||||
IP override
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setCloneOpen(true)}>
|
||||
<CopyPlusIcon className="size-4" />
|
||||
Копировать наборы
|
||||
</DropdownMenuItem>
|
||||
{a.install_curl ? (
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
copyToClipboard(a.install_curl!)
|
||||
toast.success('Скопировано')
|
||||
installRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
})
|
||||
}}
|
||||
>
|
||||
<TerminalIcon className="size-4" />
|
||||
Install curl
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
<DropdownMenuItem
|
||||
render={<Link to="/agents" />}
|
||||
>
|
||||
К списку
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
@@ -272,78 +246,26 @@ function AgentDetailPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<QuickActionGrid actions={quickActions} />
|
||||
|
||||
<DetailPanel.Section>
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<AgentLifecycleTimeline agent={a} />
|
||||
<div className="@container flex flex-col gap-4">
|
||||
<div className="grid gap-4 @4xl:grid-cols-3">
|
||||
<div className="@4xl:col-span-2">
|
||||
<AgentPolicyTrace
|
||||
preview={previewQ.data}
|
||||
isLoading={previewQ.isLoading}
|
||||
/>
|
||||
</div>
|
||||
<AgentFactsPanel agent={a} />
|
||||
</div>
|
||||
|
||||
<div ref={installRef}>
|
||||
<Frame dense spacing="sm">
|
||||
<FrameHeader>
|
||||
<FrameTitle>Install / identity</FrameTitle>
|
||||
<FrameDescription>
|
||||
Copy one-liner · hostname · token
|
||||
</FrameDescription>
|
||||
</FrameHeader>
|
||||
<FramePanel className="flex flex-col gap-3">
|
||||
{a.install_curl ? (
|
||||
<div className="flex flex-col gap-2">
|
||||
<pre className="bg-muted overflow-x-auto rounded-lg p-3 text-xs break-all whitespace-pre-wrap">
|
||||
{a.install_curl}
|
||||
</pre>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="self-start"
|
||||
onClick={() => {
|
||||
copyToClipboard(a.install_curl!)
|
||||
toast.success('Скопировано')
|
||||
}}
|
||||
>
|
||||
<Copy data-icon="inline-start" />
|
||||
Копировать
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Install curl недоступен
|
||||
</p>
|
||||
)}
|
||||
<div className="text-muted-foreground grid gap-1 text-sm">
|
||||
<div>
|
||||
Hostname:{' '}
|
||||
<span className="text-foreground">
|
||||
{a.hostname ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Last seen IP:{' '}
|
||||
<span className="text-foreground">
|
||||
{a.last_seen_ip ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Client:{' '}
|
||||
<span className="text-foreground">
|
||||
{a.client_version ?? '—'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
Token prefix:{' '}
|
||||
<span className="text-foreground font-mono">
|
||||
{a.token_prefix}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</FramePanel>
|
||||
</Frame>
|
||||
</div>
|
||||
|
||||
<div className="lg:col-span-2">
|
||||
<AgentPolicySetsSortable agentId={id} />
|
||||
</div>
|
||||
|
||||
<AgentEffectiveCidrs
|
||||
preview={previewQ.data}
|
||||
isLoading={previewQ.isLoading}
|
||||
/>
|
||||
</div>
|
||||
</DetailPanel.Section>
|
||||
</DetailPanel>
|
||||
|
||||
@@ -13,8 +13,16 @@ import {
|
||||
listTabFilter,
|
||||
} from '@/components/lists/lists-columns'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { listsQueryOptions } from '@/queries'
|
||||
import { listsQueryOptions, evobgpCommunitiesQueryOptions } from '@/queries'
|
||||
import { apiFetch } from '@/lib/api'
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteContent,
|
||||
AutocompleteEmpty,
|
||||
AutocompleteInput,
|
||||
AutocompleteItem,
|
||||
AutocompleteList,
|
||||
} from '@/components/reui/autocomplete'
|
||||
import { Button } from '@evofw/ui/components/button'
|
||||
import { Field, FieldLabel } from '@evofw/ui/components/field'
|
||||
import { Input } from '@evofw/ui/components/input'
|
||||
@@ -58,8 +66,6 @@ const CREATE_SOURCE_ITEMS = [
|
||||
function ListsPage() {
|
||||
const navigate = useNavigate()
|
||||
const qc = useQueryClient()
|
||||
const listsQ = useQuery(listsQueryOptions())
|
||||
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [name, setName] = useState('')
|
||||
const [source, setSource] = useState<CreateSource>('static')
|
||||
@@ -69,6 +75,21 @@ function ListsPage() {
|
||||
const [activeTab, setActiveTab] = useState('all')
|
||||
const [deleteListId, setDeleteListId] = useState<string | null>(null)
|
||||
|
||||
const listsQ = useQuery(listsQueryOptions())
|
||||
const communitiesQ = useQuery({
|
||||
...evobgpCommunitiesQueryOptions(),
|
||||
enabled: createOpen && source === 'evobgp_community',
|
||||
})
|
||||
|
||||
const communityItems = useMemo(
|
||||
() =>
|
||||
(communitiesQ.data?.items ?? []).map((c) => ({
|
||||
value: c.id,
|
||||
label: c.title ? `${c.community} · ${c.title}` : c.community,
|
||||
})),
|
||||
[communitiesQ.data?.items],
|
||||
)
|
||||
|
||||
const create = useMutation({
|
||||
mutationFn: async () => {
|
||||
const config: Record<string, unknown> = {}
|
||||
@@ -273,12 +294,35 @@ function ListsPage() {
|
||||
) : null}
|
||||
{source === 'evobgp_community' ? (
|
||||
<Field>
|
||||
<FieldLabel htmlFor="list-comm">Community ID</FieldLabel>
|
||||
<Input
|
||||
id="list-comm"
|
||||
<FieldLabel>BGP community</FieldLabel>
|
||||
<Autocomplete
|
||||
items={communityItems}
|
||||
value={extra}
|
||||
onChange={(e) => setExtra(e.target.value)}
|
||||
/>
|
||||
onValueChange={setExtra}
|
||||
>
|
||||
<AutocompleteInput
|
||||
placeholder={
|
||||
communitiesQ.isError
|
||||
? 'ID вручную (EvoBGP недоступен)'
|
||||
: 'Поиск community…'
|
||||
}
|
||||
showClear
|
||||
/>
|
||||
<AutocompleteContent>
|
||||
<AutocompleteEmpty>
|
||||
{communitiesQ.isLoading
|
||||
? 'Загрузка…'
|
||||
: 'Нет совпадений'}
|
||||
</AutocompleteEmpty>
|
||||
<AutocompleteList>
|
||||
{(item) => (
|
||||
<AutocompleteItem key={item.value} value={item}>
|
||||
{item.label}
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
</AutocompleteList>
|
||||
</AutocompleteContent>
|
||||
</Autocomplete>
|
||||
</Field>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,6 @@ import { DataGridPrimaryCell } from '@/components/data-grid-cell'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { PolicyRulesSortable } from '@/components/rules/policy-rules-sortable'
|
||||
import { PolicyModeToggle } from '@/components/rules/policy-mode-toggle'
|
||||
import {
|
||||
agentsQueryOptions,
|
||||
listsQueryOptions,
|
||||
@@ -103,11 +102,7 @@ function PolicySetDetailPage() {
|
||||
}, [assignedIds])
|
||||
|
||||
const patchSet = useMutation({
|
||||
mutationFn: (body: {
|
||||
enabled?: boolean
|
||||
name?: string
|
||||
policy_mode?: 'blacklist' | 'whitelist'
|
||||
}) =>
|
||||
mutationFn: (body: { enabled?: boolean; name?: string }) =>
|
||||
apiFetch(`/api/v1/policy-sets/${setId}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(body),
|
||||
@@ -277,8 +272,6 @@ function PolicySetDetailPage() {
|
||||
}
|
||||
|
||||
const set = setQ.data
|
||||
const policyMode =
|
||||
set.policy_mode === 'whitelist' ? 'whitelist' : 'blacklist'
|
||||
|
||||
return (
|
||||
<PageShell>
|
||||
@@ -339,19 +332,10 @@ function PolicySetDetailPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<DetailPanel.Section>
|
||||
<PolicyModeToggle
|
||||
value={policyMode}
|
||||
disabled={patchSet.isPending}
|
||||
onChange={(mode) => patchSet.mutate({ policy_mode: mode })}
|
||||
/>
|
||||
</DetailPanel.Section>
|
||||
|
||||
<DetailPanel.Section>
|
||||
<PolicyRulesSortable
|
||||
setId={setId}
|
||||
rules={rules}
|
||||
policyMode={policyMode}
|
||||
onDelete={(id) => setDeleteRuleId(id)}
|
||||
onAdd={() => setRuleOpen(true)}
|
||||
/>
|
||||
@@ -359,7 +343,7 @@ function PolicySetDetailPage() {
|
||||
|
||||
<DetailPanel.Section
|
||||
title="Назначено агентам"
|
||||
description="Все наборы агента должны иметь один режим фильтра."
|
||||
description="Агенты, которым применён этот набор."
|
||||
>
|
||||
<Frame dense spacing="sm">
|
||||
<FrameHeader>
|
||||
|
||||
@@ -9,7 +9,6 @@ import { PageHeader, PageShell, ResourcePage } from '@/components/reui-kit'
|
||||
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
|
||||
import { DataGridPrimaryCell } from '@/components/data-grid-cell'
|
||||
import { StatusBadge } from '@/components/status-badge'
|
||||
import { Badge } from '@/components/reui/badge'
|
||||
import { ConfirmDialog } from '@/components/confirm-dialog'
|
||||
import { PolicySetIcon } from '@/components/rules/policy-set-icon'
|
||||
import { policySetsQueryOptions } from '@/queries'
|
||||
@@ -120,7 +119,7 @@ function PolicySetsPage() {
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<PolicySetIcon mode={row.original.policy_mode} />
|
||||
<PolicySetIcon />
|
||||
<DataGridPrimaryCell
|
||||
accent="primary"
|
||||
title={row.original.name}
|
||||
@@ -140,26 +139,6 @@ function PolicySetsPage() {
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'policy_mode',
|
||||
header: ({ column }) => (
|
||||
<DataGridColumnHeader column={column} title="Режим" />
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
variant={
|
||||
row.original.policy_mode === 'whitelist'
|
||||
? 'warning-light'
|
||||
: 'secondary'
|
||||
}
|
||||
size="sm"
|
||||
>
|
||||
{row.original.policy_mode === 'whitelist'
|
||||
? 'whitelist'
|
||||
: 'blacklist'}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'rules_count',
|
||||
header: ({ column }) => (
|
||||
|
||||
Reference in New Issue
Block a user