217 lines
6.6 KiB
JavaScript
217 lines
6.6 KiB
JavaScript
import { readFileSync, writeFileSync } from "node:fs";
|
|
import { z } from "zod";
|
|
import { apiFetch } from "../api.mjs";
|
|
import { emit } from "../output.mjs";
|
|
import { t } from "../i18n.mjs";
|
|
|
|
const policyBodySchema = z.record(z.string(), z.unknown());
|
|
const importBodySchema = z.record(z.string(), z.unknown());
|
|
const contextSchema = z.record(z.string(), z.unknown());
|
|
|
|
function parseJsonInput(value, label, schema) {
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(value);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
process.stderr.write(`Invalid JSON for ${label}: ${message}\n`);
|
|
process.exit(2);
|
|
}
|
|
|
|
const result = schema.safeParse(parsed);
|
|
if (!result.success) {
|
|
process.stderr.write(
|
|
`Invalid ${label}: ${result.error.issues[0]?.message || "schema error"}\n`
|
|
);
|
|
process.exit(2);
|
|
}
|
|
return result.data;
|
|
}
|
|
|
|
function readJsonFile(file, schema) {
|
|
let raw;
|
|
try {
|
|
raw = readFileSync(file, "utf8");
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
process.stderr.write(`Unable to read ${file}: ${message}\n`);
|
|
process.exit(2);
|
|
}
|
|
return parseJsonInput(raw, file, schema);
|
|
}
|
|
|
|
function fmtTs(v) {
|
|
if (!v) return "-";
|
|
try {
|
|
return new Date(v).toLocaleString();
|
|
} catch {
|
|
return String(v);
|
|
}
|
|
}
|
|
|
|
async function confirm(q) {
|
|
return new Promise((resolve) => {
|
|
process.stdout.write(`${q} (yes/no) `);
|
|
process.stdin.setEncoding("utf8");
|
|
process.stdin.once("data", (c) => resolve(c.toString().trim().toLowerCase().startsWith("y")));
|
|
});
|
|
}
|
|
|
|
const policySchema = [
|
|
{ key: "id", header: "Policy ID", width: 22 },
|
|
{ key: "name", header: "Name", width: 30 },
|
|
{ key: "kind", header: "Kind", width: 14 },
|
|
{ key: "scope", header: "Scope", width: 16 },
|
|
{ key: "enabled", header: "Enabled", formatter: (v) => (v ? "✓" : "✗") },
|
|
{ key: "priority", header: "Prio", width: 6 },
|
|
{ key: "updatedAt", header: "Updated", formatter: fmtTs },
|
|
];
|
|
|
|
export async function runPolicyList(opts, cmd) {
|
|
const params = new URLSearchParams();
|
|
if (opts.kind) params.set("kind", opts.kind);
|
|
if (opts.scope) params.set("scope", opts.scope);
|
|
const res = await apiFetch(`/api/policies?${params}`);
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
const data = await res.json();
|
|
emit(data.items ?? data, cmd.optsWithGlobals(), policySchema);
|
|
}
|
|
|
|
export async function runPolicyGet(id, opts, cmd) {
|
|
const res = await apiFetch(`/api/policies/${id}`);
|
|
if (!res.ok) {
|
|
process.stderr.write(`Not found: ${id}\n`);
|
|
process.exit(1);
|
|
}
|
|
emit(await res.json(), cmd.optsWithGlobals());
|
|
}
|
|
|
|
export async function runPolicyCreate(opts, cmd) {
|
|
const body = readJsonFile(opts.file, policyBodySchema);
|
|
const res = await apiFetch("/api/policies", { method: "POST", body });
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
emit(await res.json(), cmd.optsWithGlobals(), policySchema);
|
|
}
|
|
|
|
export async function runPolicyUpdate(id, opts, cmd) {
|
|
const body = readJsonFile(opts.file, policyBodySchema);
|
|
const res = await apiFetch(`/api/policies/${id}`, { method: "PUT", body });
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
emit(await res.json(), cmd.optsWithGlobals(), policySchema);
|
|
}
|
|
|
|
export async function runPolicyDelete(id, opts, cmd) {
|
|
if (!opts.yes) {
|
|
const ok = await confirm(`Delete policy ${id}?`);
|
|
if (!ok) return;
|
|
}
|
|
const res = await apiFetch(`/api/policies/${id}`, { method: "DELETE" });
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
process.stdout.write("Deleted\n");
|
|
}
|
|
|
|
export async function runPolicyEvaluate(opts, cmd) {
|
|
const body = {
|
|
apiKey: opts.apiKey,
|
|
action: opts.action,
|
|
...(opts.resource ? { resource: opts.resource } : {}),
|
|
...(opts.context ? { context: parseJsonInput(opts.context, "--context", contextSchema) } : {}),
|
|
};
|
|
const res = await apiFetch("/api/policies/evaluate", { method: "POST", body });
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
const data = await res.json();
|
|
emit(data, cmd.optsWithGlobals());
|
|
process.exit(data.allowed ? 0 : 4);
|
|
}
|
|
|
|
export async function runPolicyExport(file, opts, cmd) {
|
|
const res = await apiFetch("/api/policies?export=true");
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
const data = await res.json();
|
|
writeFileSync(file, JSON.stringify(data, null, 2));
|
|
process.stdout.write(`Exported ${data.items?.length ?? 0} policies to ${file}\n`);
|
|
}
|
|
|
|
export async function runPolicyImport(file, opts, cmd) {
|
|
const body = readJsonFile(file, importBodySchema);
|
|
const overwrite = opts.overwrite ? "true" : "false";
|
|
const res = await apiFetch(`/api/policies?import=true&overwrite=${overwrite}`, {
|
|
method: "POST",
|
|
body,
|
|
});
|
|
if (!res.ok) {
|
|
process.stderr.write(`Error: ${res.status}\n`);
|
|
process.exit(1);
|
|
}
|
|
emit(await res.json(), cmd.optsWithGlobals());
|
|
}
|
|
|
|
export function registerPolicy(program) {
|
|
const policy = program.command("policy").description(t("policy.description"));
|
|
|
|
policy
|
|
.command("list")
|
|
.description(t("policy.list.description"))
|
|
.option("--kind <k>", t("policy.list.kind"))
|
|
.option("--scope <s>", t("policy.list.scope"))
|
|
.action(runPolicyList);
|
|
|
|
policy.command("get <id>").description(t("policy.get.description")).action(runPolicyGet);
|
|
|
|
policy
|
|
.command("create")
|
|
.description(t("policy.create.description"))
|
|
.requiredOption("--file <path>", t("policy.create.file"))
|
|
.action(runPolicyCreate);
|
|
|
|
policy
|
|
.command("update <id>")
|
|
.description(t("policy.update.description"))
|
|
.requiredOption("--file <path>", t("policy.update.file"))
|
|
.action(runPolicyUpdate);
|
|
|
|
policy
|
|
.command("delete <id>")
|
|
.description(t("policy.delete.description"))
|
|
.option("--yes", t("policy.delete.yes"))
|
|
.action(runPolicyDelete);
|
|
|
|
policy
|
|
.command("evaluate")
|
|
.description(t("policy.evaluate.description"))
|
|
.requiredOption("--api-key <k>", t("policy.evaluate.api_key"))
|
|
.requiredOption("--action <a>", t("policy.evaluate.action"))
|
|
.option("--resource <r>", t("policy.evaluate.resource"))
|
|
.option("--context <json>", t("policy.evaluate.context"))
|
|
.action(runPolicyEvaluate);
|
|
|
|
policy
|
|
.command("export <file>")
|
|
.description(t("policy.export.description"))
|
|
.action(runPolicyExport);
|
|
|
|
policy
|
|
.command("import <file>")
|
|
.description(t("policy.import.description"))
|
|
.option("--overwrite", t("policy.import.overwrite"))
|
|
.action(runPolicyImport);
|
|
}
|