265 lines
8.8 KiB
TypeScript
265 lines
8.8 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { apiPathPattern, mockApi, primeMockSession } from "./support/network.js";
|
|
|
|
function json(body: unknown, status = 200) {
|
|
return {
|
|
status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body),
|
|
};
|
|
}
|
|
|
|
const policyRules = [
|
|
{
|
|
id: 1,
|
|
rule_key: "failed_login_attempts",
|
|
enabled: true,
|
|
threshold_count: 5,
|
|
window_seconds: 900,
|
|
mode: "observe",
|
|
exempt_permission_nodes: ["superuser_security_limits_exempt"],
|
|
},
|
|
{
|
|
id: 2,
|
|
rule_key: "bookings_created",
|
|
enabled: true,
|
|
threshold_count: 25,
|
|
window_seconds: 86400,
|
|
mode: "observe",
|
|
exempt_permission_nodes: [],
|
|
},
|
|
{
|
|
id: 3,
|
|
rule_key: "vehicles_created",
|
|
enabled: true,
|
|
threshold_count: 20,
|
|
window_seconds: 86400,
|
|
mode: "observe",
|
|
exempt_permission_nodes: [],
|
|
},
|
|
{
|
|
id: 4,
|
|
rule_key: "requests_per_ip",
|
|
enabled: true,
|
|
threshold_count: 300,
|
|
window_seconds: 60,
|
|
mode: "observe",
|
|
exempt_permission_nodes: [],
|
|
},
|
|
{
|
|
id: 5,
|
|
rule_key: "requests_per_customer",
|
|
enabled: true,
|
|
threshold_count: 600,
|
|
window_seconds: 60,
|
|
mode: "observe",
|
|
exempt_permission_nodes: [],
|
|
},
|
|
];
|
|
|
|
const baseIncident = {
|
|
id: 7,
|
|
incident_key: "firewall:12:test",
|
|
type: "firewall_block",
|
|
severity: "high",
|
|
status: "open",
|
|
title: "Block firewall rule matched: ip 203.0.113.9",
|
|
source_ip: "203.0.113.9",
|
|
customer_number: 12345,
|
|
route_path: "/auth/login",
|
|
route_template: "/auth/login",
|
|
occurrence_count: 3,
|
|
first_seen_at: "2026-07-13 10:00:00",
|
|
last_seen_at: "2026-07-13 10:05:00",
|
|
notes: [],
|
|
};
|
|
|
|
test.describe("Superuser system security", () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.addInitScript(() => {
|
|
window.localStorage.setItem("locale", "en");
|
|
});
|
|
await mockApi(page, {
|
|
authenticated: true,
|
|
permissions: [
|
|
"superuser",
|
|
"superuser_security_view",
|
|
"superuser_security_settings_manage",
|
|
"superuser_security_firewall_manage",
|
|
"superuser_security_incidents_manage",
|
|
],
|
|
});
|
|
await primeMockSession(page, { token: "superuser-security-token", bootPath: null });
|
|
});
|
|
|
|
test("supports overview, firewall, settings, and incident management", async ({ page }) => {
|
|
const settingsPayloads: Array<Record<string, unknown>> = [];
|
|
const firewallCreatePayloads: Array<Record<string, unknown>> = [];
|
|
const incidentPatchPayloads: Array<Record<string, unknown>> = [];
|
|
const incidentNotePayloads: Array<Record<string, unknown>> = [];
|
|
let firewallRules = [
|
|
{
|
|
id: 12,
|
|
action: "watch",
|
|
target_type: "ip",
|
|
target_value: "203.0.113.9",
|
|
route_pattern: "/auth/*",
|
|
priority: 50,
|
|
reason: "Repeated login failures",
|
|
enabled: true,
|
|
expires_at: null,
|
|
},
|
|
];
|
|
let selectedIncident = { ...baseIncident };
|
|
|
|
await page.route(apiPathPattern("/superuser/system/security/summary"), async (route) => {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: {
|
|
settings: { mode: "observe", rules: policyRules },
|
|
firewall: { active_rules: firewallRules.length, block_rules: 0, watch_rules: firewallRules.length },
|
|
incidents: { open: 1, acknowledged: 0, resolved: 0, recent: [selectedIncident] },
|
|
},
|
|
meta: {},
|
|
includes: {},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/system/security/settings"), async (route) => {
|
|
if (route.request().method() === "PATCH") {
|
|
settingsPayloads.push(route.request().postDataJSON() as Record<string, unknown>);
|
|
}
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: { mode: "observe", rules: policyRules },
|
|
meta: {},
|
|
includes: {},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/system/security/firewall-rules"), async (route) => {
|
|
if (route.request().method() === "POST") {
|
|
const payload = route.request().postDataJSON() as Record<string, unknown>;
|
|
firewallCreatePayloads.push(payload);
|
|
firewallRules = [...firewallRules, { id: 13, ...payload }];
|
|
}
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: { rules: firewallRules },
|
|
meta: {},
|
|
includes: {},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/system/security/incidents"), async (route) => {
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: { incidents: [selectedIncident] },
|
|
meta: {},
|
|
includes: {},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/system/security/incidents/7"), async (route) => {
|
|
if (route.request().method() === "PATCH") {
|
|
const payload = route.request().postDataJSON() as Record<string, unknown>;
|
|
incidentPatchPayloads.push(payload);
|
|
selectedIncident = { ...selectedIncident, status: String(payload.status || selectedIncident.status) };
|
|
}
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: selectedIncident,
|
|
meta: {},
|
|
includes: {},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.route(apiPathPattern("/superuser/system/security/incidents/7/notes"), async (route) => {
|
|
const payload = route.request().postDataJSON() as Record<string, unknown>;
|
|
incidentNotePayloads.push(payload);
|
|
selectedIncident = {
|
|
...selectedIncident,
|
|
notes: [
|
|
{
|
|
id: 1,
|
|
incident_id: 7,
|
|
note: String(payload.note),
|
|
created_at: "2026-07-13 10:10:00",
|
|
},
|
|
],
|
|
};
|
|
await route.fulfill(
|
|
json({
|
|
success: true,
|
|
data: selectedIncident,
|
|
meta: {},
|
|
includes: {},
|
|
})
|
|
);
|
|
});
|
|
|
|
await page.goto("/superuser/system/security/overview");
|
|
await expect(page.getByTestId("superuser-security-page")).toBeVisible();
|
|
await expect(page.getByText("Security overview")).toBeVisible();
|
|
await expect(page.getByRole("link", { name: "Open incidents" })).toBeVisible();
|
|
await expect(page.getByText("Block firewall rule matched: ip 203.0.113.9")).toBeVisible();
|
|
|
|
await page.getByTestId("security-tab-settings").click();
|
|
await expect(page).toHaveURL(/\/superuser\/system\/security\/settings$/);
|
|
await expect(page.getByText("Max failed login attempts")).toBeVisible();
|
|
await page.getByTestId("security-setting-threshold-failed_login_attempts").fill("6");
|
|
await page
|
|
.getByTestId("security-setting-exemptions-failed_login_attempts")
|
|
.fill("superuser_security_limits_exempt, support_login_bypass");
|
|
await page.getByRole("button", { name: "Save" }).click();
|
|
await expect.poll(() => settingsPayloads.length).toBe(1);
|
|
expect(settingsPayloads[0].rules).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({
|
|
rule_key: "failed_login_attempts",
|
|
threshold_count: 6,
|
|
exempt_permission_nodes: ["superuser_security_limits_exempt", "support_login_bypass"],
|
|
}),
|
|
])
|
|
);
|
|
|
|
await page.getByTestId("security-tab-firewall").click();
|
|
await expect(page.getByText("Firewall management")).toBeVisible();
|
|
await page.getByTestId("security-firewall-action").selectOption("block");
|
|
await page.getByTestId("security-firewall-target-type").selectOption("ip");
|
|
await page.getByTestId("security-firewall-target-value").fill("198.51.100.44");
|
|
await page.getByTestId("security-firewall-route-pattern").fill("/auth/*");
|
|
await page.getByTestId("security-firewall-reason").fill("Temporary login abuse block");
|
|
await page.getByRole("button", { name: "Create" }).click();
|
|
await expect.poll(() => firewallCreatePayloads.length).toBe(1);
|
|
expect(firewallCreatePayloads[0]).toMatchObject({
|
|
action: "block",
|
|
target_type: "ip",
|
|
target_value: "198.51.100.44",
|
|
route_pattern: "/auth/*",
|
|
});
|
|
|
|
await page.getByTestId("security-tab-incidents").click();
|
|
await expect(page.getByText("Incident management")).toBeVisible();
|
|
await page.getByText("Block firewall rule matched: ip 203.0.113.9").click();
|
|
await expect(page.getByTestId("security-incident-detail")).toContainText("203.0.113.9");
|
|
await page.getByRole("button", { name: "Resolve" }).click();
|
|
await expect.poll(() => incidentPatchPayloads.length).toBe(1);
|
|
expect(incidentPatchPayloads[0]).toMatchObject({ status: "resolved" });
|
|
await page.getByTestId("security-incident-note").fill("Confirmed temporary abusive source.");
|
|
await page.getByRole("button", { name: "Add note" }).click();
|
|
await expect.poll(() => incidentNotePayloads.length).toBe(1);
|
|
expect(incidentNotePayloads[0]).toMatchObject({ note: "Confirmed temporary abusive source." });
|
|
});
|
|
});
|