mirror of
https://github.com/hansjone/netx.git
synced 2026-10-09 04:20:45 +08:00
Add opt-in per-NE CLI exec_policy for MCP/API exec.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
bfac0b53c8
commit
b9be545a75
20 changed files with 463 additions and 24 deletions
|
|
@ -14,6 +14,7 @@ import {
|
|||
buildManagedNeSaveBody,
|
||||
emptyManagedNeForm,
|
||||
formFromManagedNe,
|
||||
isLinuxDeviceType,
|
||||
type ManagedNeFormState,
|
||||
} from "./formState";
|
||||
|
||||
|
|
@ -56,6 +57,7 @@ export function ManagedNeFormDialog({
|
|||
}, [open, editingId]);
|
||||
|
||||
const vendors = metaQuery.data?.vendors ?? [];
|
||||
const execPolicyEnabled = Boolean(metaQuery.data?.exec_policy_enabled);
|
||||
const deviceTypes = useMemo(() => {
|
||||
const base = metaQuery.data?.device_types ?? [];
|
||||
const cur = String(form.device_type || "").trim();
|
||||
|
|
@ -70,6 +72,7 @@ export function ManagedNeFormDialog({
|
|||
hopHostRequired: t("managedNe.hop.hostRequired"),
|
||||
hopUserRequired: t("managedNe.hop.userRequired"),
|
||||
hopPasswordRequired: t("managedNe.hop.passwordRequired"),
|
||||
execPolicyEnabled: Boolean(metaQuery.data?.exec_policy_enabled),
|
||||
});
|
||||
if (editing) {
|
||||
return updateManagedNe(editing.id, body);
|
||||
|
|
@ -131,7 +134,14 @@ export function ManagedNeFormDialog({
|
|||
label={t("managedNe.col.deviceType")}
|
||||
required
|
||||
value={form.device_type}
|
||||
onChange={(e) => setForm({ ...form, device_type: e.target.value })}
|
||||
onChange={(e) => {
|
||||
const device_type = e.target.value;
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
device_type,
|
||||
exec_policy: isLinuxDeviceType(device_type) ? prev.exec_policy : "readonly",
|
||||
}));
|
||||
}}
|
||||
>
|
||||
{deviceTypes.map((dt) => (
|
||||
<option key={dt} value={dt}>
|
||||
|
|
@ -139,6 +149,29 @@ export function ManagedNeFormDialog({
|
|||
</option>
|
||||
))}
|
||||
</FieldSelect>
|
||||
{execPolicyEnabled ? (
|
||||
<>
|
||||
<FieldSelect
|
||||
label={t("managedNe.col.execPolicy")}
|
||||
value={form.exec_policy}
|
||||
onChange={(e) =>
|
||||
setForm({
|
||||
...form,
|
||||
exec_policy: e.target.value as ManagedNeFormState["exec_policy"],
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="readonly">{t("managedNe.execPolicy.readonly")}</option>
|
||||
{isLinuxDeviceType(form.device_type) ? (
|
||||
<>
|
||||
<option value="linux_shell">{t("managedNe.execPolicy.linuxShell")}</option>
|
||||
<option value="unrestricted">{t("managedNe.execPolicy.unrestricted")}</option>
|
||||
</>
|
||||
) : null}
|
||||
</FieldSelect>
|
||||
<p className="form-field-hint form-grid__full">{t("managedNe.execPolicy.hint")}</p>
|
||||
</>
|
||||
) : null}
|
||||
<TextField
|
||||
fullWidth
|
||||
isRequired
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export type ManagedNeFormState = {
|
|||
hop_vrf: string;
|
||||
hop_target_auth_mode: "bastion_managed" | "manual";
|
||||
hop_enter_system_view: boolean;
|
||||
exec_policy: "readonly" | "linux_shell" | "unrestricted";
|
||||
};
|
||||
|
||||
export function deviceTypeForVendor(vendor: string): string {
|
||||
|
|
@ -38,6 +39,13 @@ export function deviceTypeForVendor(vendor: string): string {
|
|||
return "generic";
|
||||
}
|
||||
|
||||
export function isLinuxDeviceType(deviceType: string | undefined | null): boolean {
|
||||
const low = String(deviceType || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return low === "linux" || low === "linux_ssh" || low === "linux_telnet" || low.startsWith("linux_");
|
||||
}
|
||||
|
||||
export function emptyManagedNeForm(): ManagedNeFormState {
|
||||
return {
|
||||
name: "",
|
||||
|
|
@ -61,6 +69,7 @@ export function emptyManagedNeForm(): ManagedNeFormState {
|
|||
hop_vrf: "",
|
||||
hop_target_auth_mode: "bastion_managed",
|
||||
hop_enter_system_view: false,
|
||||
exec_policy: "readonly",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +121,10 @@ export function formFromManagedNe(row: ManagedNeItem): ManagedNeFormState {
|
|||
hop_vrf: row.hop_vrf,
|
||||
hop_target_auth_mode: row.hop_target_auth_mode === "manual" ? "manual" : "bastion_managed",
|
||||
hop_enter_system_view: Boolean(row.hop_enter_system_view),
|
||||
exec_policy:
|
||||
row.exec_policy === "linux_shell" || row.exec_policy === "unrestricted"
|
||||
? row.exec_policy
|
||||
: "readonly",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +140,14 @@ export function managedSourceKey(
|
|||
/** Build API body + hop validation. Throws Error with message key text already translated by caller. */
|
||||
export function buildManagedNeSaveBody(
|
||||
form: ManagedNeFormState,
|
||||
opts: { editing: boolean; hopHostRequired: string; hopUserRequired: string; hopPasswordRequired: string },
|
||||
opts: {
|
||||
editing: boolean;
|
||||
hopHostRequired: string;
|
||||
hopUserRequired: string;
|
||||
hopPasswordRequired: string;
|
||||
/** When false, omit exec_policy so lab-open cannot be persisted accidentally. */
|
||||
execPolicyEnabled?: boolean;
|
||||
},
|
||||
): Record<string, unknown> {
|
||||
const body: Record<string, unknown> = {
|
||||
name: form.name,
|
||||
|
|
@ -152,6 +172,9 @@ export function buildManagedNeSaveBody(
|
|||
...(form.password ? { password: form.password } : {}),
|
||||
...(form.hop_password ? { hop_password: form.hop_password } : {}),
|
||||
};
|
||||
if (opts.execPolicyEnabled) {
|
||||
body.exec_policy = isLinuxDeviceType(form.device_type) ? form.exec_policy : "readonly";
|
||||
}
|
||||
if (form.hop_enabled) {
|
||||
if (!form.hop_host.trim()) throw new Error(opts.hopHostRequired);
|
||||
if (!form.hop_username.trim()) throw new Error(opts.hopUserRequired);
|
||||
|
|
|
|||
|
|
@ -495,6 +495,7 @@ export const fetchManagedNeMeta = () =>
|
|||
device_types: types.device_types,
|
||||
vendors: types.vendors,
|
||||
credentials_configured: creds.configured,
|
||||
exec_policy_enabled: Boolean(types.exec_policy_enabled),
|
||||
}));
|
||||
|
||||
export type ManagedNeStats = {
|
||||
|
|
|
|||
|
|
@ -211,6 +211,8 @@ export type ManagedNeItem = {
|
|||
hop_vrf: string;
|
||||
hop_target_auth_mode: string;
|
||||
hop_enter_system_view?: boolean;
|
||||
/** MCP/API CLI gate: readonly | linux_shell | unrestricted */
|
||||
exec_policy?: "readonly" | "linux_shell" | "unrestricted";
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
|
@ -225,6 +227,8 @@ export type ManagedNeListResponse = {
|
|||
export type ManagedNeMeta = {
|
||||
device_types: string[];
|
||||
vendors: string[];
|
||||
/** When false (default), exec_policy UI/API open policies are disabled. */
|
||||
exec_policy_enabled?: boolean;
|
||||
};
|
||||
|
||||
export type ManagedNeImportResult = {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue