fix(permissions): validate network access flow
This commit is contained in:
@ -122,7 +122,8 @@ describe("AgentsPanel (with MockAgentGateway)", () => {
|
||||
wanted: "allow",
|
||||
effective: "deny",
|
||||
runtimeLock: { state: "locked", reason: "Runtime locked." },
|
||||
control: { mode: "readOnly", reason: "Runtime locked." },
|
||||
control: { mode: "editable" },
|
||||
runtimeControl: { mode: "readOnly", reason: "Runtime locked." },
|
||||
});
|
||||
|
||||
renderPanel(agent, new MockProfileGateway(), "/home/me/proj", undefined, permission);
|
||||
|
||||
@ -344,7 +344,9 @@ function NetworkPermissionEditor({
|
||||
const wanted = resolved?.wanted ?? source?.network ?? inherited?.network ?? null;
|
||||
const effective = resolved?.effective ?? null;
|
||||
const runtimeLocked = resolved?.runtimeLock.state === "locked";
|
||||
const lockReason = resolved?.runtimeLock.reason ?? resolved?.control.reason ?? null;
|
||||
const runtimeReadOnly = resolved?.runtimeControl.mode === "readOnly";
|
||||
const runtimeReason = resolved?.runtimeLock.reason ?? resolved?.runtimeControl.reason ?? null;
|
||||
const runtimeUnobserved = resolved?.runtimeLock.state === "none" && runtimeReadOnly;
|
||||
|
||||
return (
|
||||
<section className="rounded-md border border-border bg-surface">
|
||||
@ -371,7 +373,15 @@ function NetworkPermissionEditor({
|
||||
/>
|
||||
<StatusLine
|
||||
label="Runtime"
|
||||
value={runtimeLocked ? "Verrouillé par le runtime" : "Aucun verrou connu"}
|
||||
value={
|
||||
runtimeLocked
|
||||
? "Runtime verrouillé"
|
||||
: runtimeUnobserved
|
||||
? "Aucun runtime actif"
|
||||
: runtimeReadOnly
|
||||
? "Runtime en lecture seule"
|
||||
: "Runtime modifiable"
|
||||
}
|
||||
/>
|
||||
<StatusLine
|
||||
label="Control"
|
||||
@ -381,7 +391,19 @@ function NetworkPermissionEditor({
|
||||
|
||||
{readOnly && (
|
||||
<p className="rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning">
|
||||
{lockReason ?? "Le runtime actif ne permet pas de modifier cette permission."}
|
||||
{resolved?.control.reason ?? "Cette configuration réseau est en lecture seule."}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!readOnly && runtimeUnobserved && (
|
||||
<p className="rounded-md border border-primary/30 bg-primary/10 px-3 py-2 text-xs text-primary">
|
||||
Aucun runtime actif. La configuration sera appliquée au prochain lancement.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!readOnly && runtimeLocked && (
|
||||
<p className="rounded-md border border-warning/40 bg-warning/10 px-3 py-2 text-xs text-warning">
|
||||
{runtimeReason ?? "Le runtime actif expose un état réseau en lecture seule."}
|
||||
</p>
|
||||
)}
|
||||
|
||||
|
||||
@ -102,7 +102,7 @@ describe("PermissionsPanel", () => {
|
||||
expect(screen.getAllByText("Réseau autorisé").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders the network control read-only when the resolved runtime says so", async () => {
|
||||
it("renders the network control read-only when persisted control says so", async () => {
|
||||
const agent = new MockAgentGateway();
|
||||
const permission = new MockPermissionGateway();
|
||||
const created = await agent.createAgent(PROJECT_ID, {
|
||||
@ -118,6 +118,7 @@ describe("PermissionsPanel", () => {
|
||||
reason: "Runtime network is locked.",
|
||||
},
|
||||
control: { mode: "readOnly", reason: "Runtime network is locked." },
|
||||
runtimeControl: { mode: "readOnly", reason: "Runtime network is locked." },
|
||||
});
|
||||
const gateways = {
|
||||
agent,
|
||||
@ -132,7 +133,7 @@ describe("PermissionsPanel", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Verrouillé par le runtime")).toBeTruthy();
|
||||
expect(screen.getByText("Runtime verrouillé")).toBeTruthy();
|
||||
});
|
||||
expect(screen.getByText("Runtime network is locked.")).toBeTruthy();
|
||||
expect(
|
||||
@ -140,4 +141,95 @@ describe("PermissionsPanel", () => {
|
||||
.disabled,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps wanted network editable when no runtime is active", async () => {
|
||||
const { permission } = await renderPanel();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Aucun runtime actif")).toBeTruthy();
|
||||
});
|
||||
expect(
|
||||
screen.getByText(
|
||||
"Aucun runtime actif. La configuration sera appliquée au prochain lancement.",
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
(screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement)
|
||||
.disabled,
|
||||
).toBe(false);
|
||||
|
||||
fireEvent.change(screen.getByLabelText("Réseau — defaults projet network"), {
|
||||
target: { value: "deny" },
|
||||
});
|
||||
fireEvent.click(screen.getByRole("button", { name: "Save network" }));
|
||||
|
||||
await waitFor(async () => {
|
||||
const doc = await permission.getProjectSystemPermissions(PROJECT_ID);
|
||||
expect(doc.projectDefault?.network).toBe("deny");
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps an idle agent editable by default and only enables Save after a change", async () => {
|
||||
await renderPanel();
|
||||
|
||||
fireEvent.click(screen.getByText("Builder"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Aucun runtime actif")).toBeTruthy();
|
||||
});
|
||||
expect(screen.queryByText("Lecture seule")).toBeNull();
|
||||
expect(screen.queryByText("Runtime verrouillé")).toBeNull();
|
||||
|
||||
const select = screen.getByLabelText("Réseau — Builder network") as HTMLSelectElement;
|
||||
const save = screen.getByRole("button", { name: "Save network" });
|
||||
|
||||
expect(select.disabled).toBe(false);
|
||||
expect(save).toHaveProperty("disabled", true);
|
||||
|
||||
fireEvent.change(select, { target: { value: "allow" } });
|
||||
|
||||
expect(save).toHaveProperty("disabled", false);
|
||||
});
|
||||
|
||||
it("keeps wanted network editable when only runtime control is read-only", async () => {
|
||||
const agent = new MockAgentGateway();
|
||||
const permission = new MockPermissionGateway();
|
||||
const created = await agent.createAgent(PROJECT_ID, {
|
||||
name: "Builder",
|
||||
profileId: "p1",
|
||||
});
|
||||
permission.setResolvedAgentSystemPermissions(created.id, {
|
||||
wanted: "allow",
|
||||
effective: "deny",
|
||||
runtimeLock: {
|
||||
state: "locked",
|
||||
source: "external-runtime",
|
||||
reason: "IdeA cannot change this active runtime.",
|
||||
},
|
||||
control: { mode: "editable" },
|
||||
runtimeControl: {
|
||||
mode: "readOnly",
|
||||
reason: "IdeA cannot change this active runtime.",
|
||||
},
|
||||
});
|
||||
const gateways = {
|
||||
agent,
|
||||
permission,
|
||||
profile: new MockProfileGateway(),
|
||||
} as unknown as Gateways;
|
||||
|
||||
render(
|
||||
<DIProvider gateways={gateways}>
|
||||
<PermissionsPanel projectId={PROJECT_ID} />
|
||||
</DIProvider>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Runtime verrouillé")).toBeTruthy();
|
||||
});
|
||||
expect(
|
||||
(screen.getByLabelText("Réseau — defaults projet network") as HTMLSelectElement)
|
||||
.disabled,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@ -61,7 +61,8 @@ describe("TerminalView (with MockTerminalGateway)", () => {
|
||||
wanted: "allow",
|
||||
effective: "deny",
|
||||
runtimeLock: { state: "locked", reason: "Runtime network locked." },
|
||||
control: { mode: "readOnly", reason: "Runtime network locked." },
|
||||
control: { mode: "editable" },
|
||||
runtimeControl: { mode: "readOnly", reason: "Runtime network locked." },
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user