offer the log mode in the swarm ui
This commit is contained in:
parent
b74011bb54
commit
9140d063d7
3 changed files with 88 additions and 28 deletions
|
|
@ -27,7 +27,9 @@ import {
|
|||
editorSelection,
|
||||
grantSummary,
|
||||
internetBody,
|
||||
internetMode,
|
||||
internetSummary,
|
||||
type InternetMode,
|
||||
} from "./permissions.js";
|
||||
import "./AgentLimits.css";
|
||||
|
||||
|
|
@ -35,6 +37,7 @@ import "./AgentLimits.css";
|
|||
interface Grant {
|
||||
permissions?: string[];
|
||||
unrestricted_internet?: boolean;
|
||||
internet_log?: boolean;
|
||||
}
|
||||
|
||||
// Mirrors swarm-controller's `Catalogue` (`GET /api/permissions`).
|
||||
|
|
@ -43,6 +46,17 @@ interface Catalogue {
|
|||
default: string[];
|
||||
}
|
||||
|
||||
// The buttons for the modes an agent is not in: mode, label, tooltip.
|
||||
const INTERNET_MODES: [InternetMode, string, string][] = [
|
||||
[
|
||||
"unrestricted",
|
||||
"allow unrestricted",
|
||||
"let its traffic leave the host unfiltered",
|
||||
],
|
||||
["log", "log", "allow what restricted would deny, and log it as would_deny"],
|
||||
["restricted", "restrict", "allow only the listed names"],
|
||||
];
|
||||
|
||||
function permissionsUrl(agent: string): string {
|
||||
return `/api/agents/${encodeURIComponent(agent)}/permissions`;
|
||||
}
|
||||
|
|
@ -75,15 +89,15 @@ export function AgentPermissions({ agent }: { agent: string }) {
|
|||
};
|
||||
}, [agent, reload]);
|
||||
|
||||
// Flips the internet flag, sending the stored list with it.
|
||||
async function setInternet(g: Grant, unrestricted: boolean) {
|
||||
// Sets the internet mode, sending the stored list with it.
|
||||
async function setInternet(g: Grant, mode: InternetMode) {
|
||||
if (flagBusy) return;
|
||||
setFlagBusy(true);
|
||||
try {
|
||||
const r = await fetch(permissionsUrl(agent), {
|
||||
method: "PUT",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(internetBody(g.permissions, unrestricted)),
|
||||
body: JSON.stringify(internetBody(g.permissions, mode)),
|
||||
});
|
||||
if (!r.ok) {
|
||||
setError(await readApiError(r));
|
||||
|
|
@ -130,20 +144,21 @@ export function AgentPermissions({ agent }: { agent: string }) {
|
|||
) : null}
|
||||
<Badge
|
||||
label="internet"
|
||||
value={internetSummary(grant.unrestricted_internet)}
|
||||
/>
|
||||
<Badge
|
||||
variant="quiet"
|
||||
value={
|
||||
grant.unrestricted_internet === false
|
||||
? "allow unrestricted"
|
||||
: "restrict"
|
||||
}
|
||||
title={`set whether ${agent}'s traffic may leave the host unfiltered`}
|
||||
onClick={() =>
|
||||
void setInternet(grant, grant.unrestricted_internet === false)
|
||||
}
|
||||
value={internetSummary(grant.unrestricted_internet, grant.internet_log)}
|
||||
/>
|
||||
{INTERNET_MODES.filter(
|
||||
([mode]) =>
|
||||
mode !==
|
||||
internetMode(grant.unrestricted_internet, grant.internet_log),
|
||||
).map(([mode, label, title]) => (
|
||||
<Badge
|
||||
key={mode}
|
||||
variant="quiet"
|
||||
value={label}
|
||||
title={`${title} for ${agent}`}
|
||||
onClick={() => void setInternet(grant, mode)}
|
||||
/>
|
||||
))}
|
||||
<Badge
|
||||
variant="quiet"
|
||||
value="edit"
|
||||
|
|
|
|||
|
|
@ -22,26 +22,50 @@ export function grantSummary(stored: string[] | undefined): string {
|
|||
return `${stored.length} granted`;
|
||||
}
|
||||
|
||||
/** The three modes of an agent's internet access. */
|
||||
export type InternetMode = "unrestricted" | "log" | "restricted";
|
||||
|
||||
/**
|
||||
* How the panel reads the stored `unrestricted_internet`. An absent value
|
||||
* counts as on, and says "legacy" because the swarm never set it.
|
||||
* How the panel reads the stored `unrestricted_internet` and `internet_log`.
|
||||
* An absent flag counts as on, and says "legacy" because the swarm never set
|
||||
* it. `log` is `internet_log` beside an explicit `false`.
|
||||
*/
|
||||
export function internetSummary(stored: boolean | undefined): string {
|
||||
export function internetSummary(
|
||||
stored: boolean | undefined,
|
||||
log?: boolean,
|
||||
): string {
|
||||
if (stored === undefined) return "unrestricted (legacy)";
|
||||
return stored ? "unrestricted" : "restricted";
|
||||
if (stored) return "unrestricted";
|
||||
return log ? "log (unlisted names allowed, logged)" : "restricted";
|
||||
}
|
||||
|
||||
/** The mode the stored fields put the agent in. */
|
||||
export function internetMode(
|
||||
stored: boolean | undefined,
|
||||
log?: boolean,
|
||||
): InternetMode {
|
||||
if (stored === undefined || stored) return "unrestricted";
|
||||
return log ? "log" : "restricted";
|
||||
}
|
||||
|
||||
/**
|
||||
* The `PUT` body that sets the internet flag. A `PUT` without `permissions`
|
||||
* The `PUT` body that sets the internet mode. A `PUT` without `permissions`
|
||||
* resets a stored list to the default, so the stored list goes along; an
|
||||
* agent with none stored sends the flag alone, which leaves it unset.
|
||||
* agent with none stored sends the mode alone, which leaves the list unset.
|
||||
* `log` names `internet_log` beside `unrestricted_internet: false`; the other
|
||||
* two leave it out, which ends logging.
|
||||
*/
|
||||
export function internetBody(
|
||||
stored: string[] | undefined,
|
||||
unrestricted: boolean,
|
||||
): { permissions?: string[]; unrestricted_internet: boolean } {
|
||||
mode: InternetMode,
|
||||
): {
|
||||
permissions?: string[];
|
||||
unrestricted_internet: boolean;
|
||||
internet_log?: true;
|
||||
} {
|
||||
return {
|
||||
...(stored === undefined ? {} : { permissions: stored }),
|
||||
unrestricted_internet: unrestricted,
|
||||
unrestricted_internet: mode === "unrestricted",
|
||||
...(mode === "log" ? { internet_log: true as const } : {}),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
editorSelection,
|
||||
grantSummary,
|
||||
internetBody,
|
||||
internetMode,
|
||||
internetSummary,
|
||||
} from "../src/agent/detail/permissions.ts";
|
||||
|
||||
|
|
@ -43,18 +44,38 @@ test("an absent internet flag reads as unrestricted, marked legacy", () => {
|
|||
});
|
||||
|
||||
test("the internet toggle sends the stored list with the flag", () => {
|
||||
assert.deepEqual(internetBody(["meta", "inbox"], false), {
|
||||
assert.deepEqual(internetBody(["meta", "inbox"], "restricted"), {
|
||||
permissions: ["meta", "inbox"],
|
||||
unrestricted_internet: false,
|
||||
});
|
||||
assert.deepEqual(internetBody([], true), {
|
||||
assert.deepEqual(internetBody([], "unrestricted"), {
|
||||
permissions: [],
|
||||
unrestricted_internet: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("with no stored list the toggle leaves the list unset", () => {
|
||||
assert.deepEqual(internetBody(undefined, false), {
|
||||
assert.deepEqual(internetBody(undefined, "restricted"), {
|
||||
unrestricted_internet: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("log is internet_log beside an explicit false, and nothing else reads as it", () => {
|
||||
assert.equal(internetMode(false, true), "log");
|
||||
assert.equal(internetMode(false, undefined), "restricted");
|
||||
assert.equal(internetMode(false, false), "restricted");
|
||||
assert.equal(internetMode(true, true), "unrestricted");
|
||||
assert.equal(internetMode(undefined, true), "unrestricted");
|
||||
assert.match(internetSummary(false, true), /^log /);
|
||||
assert.equal(internetSummary(false), "restricted");
|
||||
});
|
||||
|
||||
test("the log mode keeps the old flag false and names internet_log; the others leave it out", () => {
|
||||
assert.deepEqual(internetBody(["meta"], "log"), {
|
||||
permissions: ["meta"],
|
||||
unrestricted_internet: false,
|
||||
internet_log: true,
|
||||
});
|
||||
assert.equal("internet_log" in internetBody([], "restricted"), false);
|
||||
assert.equal("internet_log" in internetBody([], "unrestricted"), false);
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue