swarm-ui: the egress card on the swarm page lists and edits the swarm-wide denied domains, and says which hives have not applied them
This commit is contained in:
parent
0bc350a8a1
commit
0421f28d49
3 changed files with 210 additions and 103 deletions
|
|
@ -32,6 +32,7 @@ import { Panel } from "../../ui/panel/Panel.js";
|
|||
import { TextField } from "../../ui/text-field/TextField.js";
|
||||
import {
|
||||
ALLOWED_DOMAINS_URL,
|
||||
DENIED_DOMAINS_URL,
|
||||
entriesFor,
|
||||
entryBody,
|
||||
entryGroups,
|
||||
|
|
@ -150,17 +151,20 @@ export function AllowedDomains({ agent }: { agent: string }) {
|
|||
);
|
||||
}
|
||||
|
||||
// The add form of one allowed domain: for `agent`, or for every agent. With no
|
||||
// `agent` (the swarm page) it can only add a swarm-wide entry.
|
||||
// The add form of one allowed or denied domain: for `agent`, or for every
|
||||
// agent. With no `agent` (the swarm page) it can only add a swarm-wide entry.
|
||||
export function AddForm({
|
||||
agent,
|
||||
kind = "allow",
|
||||
onClose,
|
||||
onAdded,
|
||||
}: {
|
||||
agent?: string;
|
||||
kind?: "allow" | "deny";
|
||||
onClose: () => void;
|
||||
onAdded: () => void;
|
||||
}) {
|
||||
const deny = kind === "deny";
|
||||
const [domain, setDomain] = useState("");
|
||||
const [forSwarm, setForSwarm] = useState(agent === undefined);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
|
@ -171,7 +175,7 @@ export function AddForm({
|
|||
setBusy(true);
|
||||
setProblem(null);
|
||||
try {
|
||||
const r = await fetch(ALLOWED_DOMAINS_URL, {
|
||||
const r = await fetch(deny ? DENIED_DOMAINS_URL : ALLOWED_DOMAINS_URL, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(entryBody(domain, forSwarm, agent ?? "")),
|
||||
|
|
@ -191,28 +195,30 @@ export function AddForm({
|
|||
|
||||
return (
|
||||
<Panel
|
||||
title={
|
||||
agent === undefined
|
||||
? "allowed domain — swarm-wide"
|
||||
: `allowed domain — ${agent}`
|
||||
}
|
||||
title={`${deny ? "denied" : "allowed"} domain — ${agent ?? "swarm-wide"}`}
|
||||
icon="🌐"
|
||||
onClose={onClose}
|
||||
>
|
||||
<p>
|
||||
{agent === undefined
|
||||
? "Lets every agent reach one more name through its hive's proxy."
|
||||
: `Lets ${agent} reach one more name through its hive's proxy, or every agent when the box below is ticked.`}{" "}
|
||||
{deny
|
||||
? agent === undefined
|
||||
? "Stops every agent reaching one name through its hive's proxy."
|
||||
: `Stops ${agent} reaching one name through its hive's proxy, or every agent when the box below is ticked.`
|
||||
: agent === undefined
|
||||
? "Lets every agent reach one more name through its hive's proxy."
|
||||
: `Lets ${agent} reach one more name through its hive's proxy, or every agent when the box below is ticked.`}{" "}
|
||||
A name needs at least two labels, such as <code>github.com</code>; a
|
||||
leading dot, as in <code>.githubusercontent.com</code>, also allows
|
||||
every subdomain. IP addresses and wildcards are refused. A name that
|
||||
resolves to a private address stays blocked. The entry adds to the names
|
||||
each hive's own configuration allows. The hive reads a change within
|
||||
about 30 seconds.
|
||||
leading dot, as in <code>.githubusercontent.com</code>, also{" "}
|
||||
{deny ? "refuses" : "allows"} every subdomain. IP addresses and
|
||||
wildcards are refused.{" "}
|
||||
{deny
|
||||
? "A denied name is refused in every egress mode, even when a swarm entry or a hive's own configuration allows it."
|
||||
: "A name that resolves to a private address stays blocked. The entry adds to the names each hive's own configuration allows."}{" "}
|
||||
The hive reads a change within about 30 seconds.
|
||||
</p>
|
||||
<form class="ui-agent-limits-form" onSubmit={(e) => void save(e)}>
|
||||
<TextField
|
||||
id="allowed-domain"
|
||||
id={deny ? "denied-domain" : "allowed-domain"}
|
||||
label="domain"
|
||||
value={domain}
|
||||
placeholder="e.g. github.com"
|
||||
|
|
@ -220,7 +226,7 @@ export function AddForm({
|
|||
/>
|
||||
{agent === undefined ? null : (
|
||||
<CheckboxField
|
||||
id="allowed-domain-swarm"
|
||||
id={deny ? "denied-domain-swarm" : "allowed-domain-swarm"}
|
||||
label="for every agent in the swarm"
|
||||
checked={forSwarm}
|
||||
onChange={setForSwarm}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface AllowedDomain {
|
|||
}
|
||||
|
||||
export const ALLOWED_DOMAINS_URL = "/api/egress/allowed-domains";
|
||||
export const DENIED_DOMAINS_URL = "/api/egress/denied-domains";
|
||||
|
||||
/**
|
||||
* The entries that apply to `agent`: the swarm-wide ones first, then the ones
|
||||
|
|
@ -92,3 +93,16 @@ export function entryBody(
|
|||
export function swarmWideEntries(entries: AllowedDomain[]): AllowedDomain[] {
|
||||
return entries.filter((e) => e.agent === undefined && e.derived !== true);
|
||||
}
|
||||
|
||||
/**
|
||||
* What the deny list says about hives that have not taken its current
|
||||
* revision: a hive in this list does not refuse the names yet. `null` when
|
||||
* every hive has.
|
||||
*/
|
||||
export function behindNote(behind: string[]): string | null {
|
||||
if (behind.length === 0) return null;
|
||||
const names = behind.join(", ");
|
||||
return behind.length === 1
|
||||
? `${names} has not applied this list yet, so its proxy may still let these names through.`
|
||||
: `${names} have not applied this list yet, so their proxies may still let these names through.`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,28 @@
|
|||
// The swarm's allowed-domain entries and the one change that is not a form:
|
||||
// removing an entry. Shared by an agent card's allowed domains and the swarm
|
||||
// page's egress card, which differ only in which entries they show.
|
||||
// The swarm's allowed-domain and denied-domain entries and the one change that
|
||||
// is not a form: removing an entry. Shared by an agent card's allowed domains
|
||||
// and the swarm page's egress card, which differ only in which entries they
|
||||
// show.
|
||||
//
|
||||
// Reads `GET /api/egress/allowed-domains` (every entry) and removes one with a
|
||||
// `DELETE` carrying the entry, each answering the whole list. `refetch` reads
|
||||
// the list again, for a caller that added an entry through its own form.
|
||||
// Reads `GET <url>` (every entry) and removes one with a `DELETE` carrying the
|
||||
// entry, each answering the whole list. `refetch` reads the list again, for a
|
||||
// caller that added an entry through its own form. The deny list also reports
|
||||
// `behind`, the hives that have not taken its current revision yet.
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { readApiError, type ProblemDetails } from "@hive/shared/api-error.js";
|
||||
import { ALLOWED_DOMAINS_URL, type AllowedDomain } from "./allowed-domains.js";
|
||||
import {
|
||||
ALLOWED_DOMAINS_URL,
|
||||
DENIED_DOMAINS_URL,
|
||||
type AllowedDomain,
|
||||
} from "./allowed-domains.js";
|
||||
|
||||
interface Listing {
|
||||
entries: AllowedDomain[];
|
||||
behind?: string[];
|
||||
}
|
||||
|
||||
export function useAllowedDomains() {
|
||||
function useDomainList(url: string) {
|
||||
const [entries, setEntries] = useState<AllowedDomain[] | null>(null);
|
||||
const [behind, setBehind] = useState<string[]>([]);
|
||||
const [error, setError] = useState<ProblemDetails | null>(null);
|
||||
// Bumped after an add or a removal, so the list is fetched again.
|
||||
const [reload, setReload] = useState(0);
|
||||
|
|
@ -23,7 +31,7 @@ export function useAllowedDomains() {
|
|||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const r = await fetch(ALLOWED_DOMAINS_URL);
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) {
|
||||
if (!cancelled) setError(await readApiError(r));
|
||||
return;
|
||||
|
|
@ -31,6 +39,7 @@ export function useAllowedDomains() {
|
|||
const body = (await r.json()) as Listing;
|
||||
if (cancelled) return;
|
||||
setEntries(body.entries);
|
||||
setBehind(body.behind ?? []);
|
||||
setError(null);
|
||||
})().catch((e: unknown) => {
|
||||
if (!cancelled) setError({ detail: String(e) });
|
||||
|
|
@ -38,13 +47,13 @@ export function useAllowedDomains() {
|
|||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [reload]);
|
||||
}, [url, reload]);
|
||||
|
||||
async function remove(entry: AllowedDomain) {
|
||||
if (busy) return;
|
||||
setBusy(true);
|
||||
try {
|
||||
const r = await fetch(ALLOWED_DOMAINS_URL, {
|
||||
const r = await fetch(url, {
|
||||
method: "DELETE",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(entry),
|
||||
|
|
@ -63,9 +72,13 @@ export function useAllowedDomains() {
|
|||
|
||||
return {
|
||||
entries,
|
||||
behind,
|
||||
error,
|
||||
busy,
|
||||
remove,
|
||||
refetch: () => setReload((n) => n + 1),
|
||||
};
|
||||
}
|
||||
|
||||
export const useAllowedDomains = () => useDomainList(ALLOWED_DOMAINS_URL);
|
||||
export const useDeniedDomains = () => useDomainList(DENIED_DOMAINS_URL);
|
||||
|
|
|
|||
Loading…
Reference in a new issue