forked from baron/baron-sso
265 lines
8.2 KiB
TypeScript
265 lines
8.2 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import {
|
|
type Consent,
|
|
installDevApiMock,
|
|
makeClient,
|
|
seedAuth,
|
|
} from "./helpers/devfront-fixtures";
|
|
import { captureEvidence } from "./helpers/evidence";
|
|
|
|
test.describe("DevFront security and isolation", () => {
|
|
test.afterEach(async ({ page }, testInfo) => {
|
|
if (testInfo.status === "passed") {
|
|
await captureEvidence(page, testInfo, testInfo.title);
|
|
}
|
|
});
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
page.on("dialog", async (dialog) => {
|
|
await dialog.accept();
|
|
});
|
|
await seedAuth(page);
|
|
});
|
|
|
|
test("tenant isolation: forbidden client shows blocked error", async ({
|
|
page,
|
|
}) => {
|
|
const state = {
|
|
clients: [makeClient("tenant-a-client", { name: "Tenant A app" })],
|
|
consents: [] as Consent[],
|
|
auditLogsByCursor: undefined,
|
|
};
|
|
await installDevApiMock(page, state);
|
|
|
|
await page.goto("/clients/tenant-b-client");
|
|
await expect(
|
|
page.getByText(
|
|
/Error loading (app|client)|앱 정보를 불러오지 못했습니다|클라이언트 정보를 불러오지 못했습니다/i,
|
|
),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("RBAC: user without manage_all permission should not see private apps", async ({
|
|
page,
|
|
}) => {
|
|
const state = {
|
|
clients: [
|
|
makeClient("pkce-client", {
|
|
name: "PKCE only app",
|
|
type: "pkce",
|
|
}),
|
|
],
|
|
consents: [] as Consent[],
|
|
auditLogsByCursor: undefined,
|
|
};
|
|
await installDevApiMock(page, state);
|
|
|
|
await page.goto("/clients");
|
|
await expect(page.getByText("PKCE only app")).toBeVisible();
|
|
await expect(page.getByText("Server side App")).not.toBeVisible();
|
|
});
|
|
|
|
test("user can enter DevFront and sees empty RP list", async ({ page }) => {
|
|
await seedAuth(page, "user");
|
|
const state = {
|
|
clients: [] as ReturnType<typeof makeClient>[],
|
|
consents: [] as Consent[],
|
|
auditLogsByCursor: undefined,
|
|
developerRequests: [
|
|
{
|
|
id: "req-audit-approved",
|
|
userId: "playwright-user",
|
|
userName: "Playwright User",
|
|
name: "Playwright User",
|
|
userEmail: "playwright@example.com",
|
|
organization: "Tenant A",
|
|
reason: "Need access",
|
|
status: "approved",
|
|
accessPages: ["audit"],
|
|
createdAt: "2026-05-29T00:00:00.000Z",
|
|
updatedAt: "2026-05-29T00:10:00.000Z",
|
|
approvedAt: "2026-05-29T00:10:00.000Z",
|
|
},
|
|
],
|
|
};
|
|
await installDevApiMock(page, state);
|
|
|
|
await page.goto("/clients");
|
|
await expect(page).toHaveURL(/\/clients$/);
|
|
await expect(
|
|
page.getByText(/조회 가능한 RP가 없습니다|No RPs are available/i),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: /연동 앱 추가|새 클라이언트|Create/i }),
|
|
).not.toBeVisible();
|
|
});
|
|
|
|
test("user receives 403 on clients list and sees ForbiddenMessage", async ({
|
|
page,
|
|
}) => {
|
|
await seedAuth(page, "user");
|
|
|
|
const state = {
|
|
clients: [] as ReturnType<typeof makeClient>[],
|
|
consents: [] as Consent[],
|
|
auditLogsByCursor: undefined,
|
|
};
|
|
await installDevApiMock(page, state);
|
|
|
|
await page.route("**/api/v1/dev/clients", async (route) => {
|
|
if (route.request().method() === "GET") {
|
|
return route.fulfill({
|
|
status: 403,
|
|
contentType: "application/json",
|
|
body: '{"error": "forbidden"}',
|
|
});
|
|
}
|
|
return route.fallback();
|
|
});
|
|
|
|
await page.goto("/clients");
|
|
await expect(
|
|
page.getByText(
|
|
/연동 앱 접근 권한 없음|Access denied: Connected Applications/i,
|
|
),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByText(
|
|
/일반 사용자 계정은 담당 RP\(앱\)에 대한 운영 또는 관리 관계가 부여된 경우에만 해당 기능을 사용할 수 있습니다|Standard user accounts can use this feature only when an operational or administrative relationship is granted for the target application/i,
|
|
),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("user receives 403 on audit logs and sees ForbiddenMessage", async ({
|
|
page,
|
|
}) => {
|
|
await seedAuth(page, "user");
|
|
|
|
const state = {
|
|
clients: [] as ReturnType<typeof makeClient>[],
|
|
consents: [] as Consent[],
|
|
auditLogsByCursor: undefined,
|
|
developerRequests: [
|
|
{
|
|
id: "req-audit-approved",
|
|
userId: "playwright-user",
|
|
userName: "Playwright User",
|
|
name: "Playwright User",
|
|
userEmail: "playwright@example.com",
|
|
organization: "Tenant A",
|
|
reason: "Need audit access",
|
|
status: "approved",
|
|
accessPages: ["audit"],
|
|
createdAt: "2026-05-29T00:00:00.000Z",
|
|
updatedAt: "2026-05-29T00:10:00.000Z",
|
|
approvedAt: "2026-05-29T00:10:00.000Z",
|
|
},
|
|
],
|
|
};
|
|
await installDevApiMock(page, state);
|
|
|
|
await page.route("**/api/v1/dev/audit-logs*", async (route) => {
|
|
if (route.request().method() === "GET") {
|
|
return route.fulfill({
|
|
status: 403,
|
|
contentType: "application/json",
|
|
body: '{"error": "forbidden"}',
|
|
});
|
|
}
|
|
return route.fallback();
|
|
});
|
|
|
|
await page.goto("/audit-logs");
|
|
await expect(
|
|
page.getByText(/감사 로그 접근 권한 없음|Access denied: Audit Logs/i),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByText(
|
|
/해당 앱\(RP\)에 대한 감사 로그 조회는 운영 또는 감사 조회 관계가 부여된 경우에만 사용할 수 있습니다|Viewing audit logs for this application requires an audit read relationship/i,
|
|
),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("user sees audit log access CTA when access is blocked", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await seedAuth(page, "user");
|
|
|
|
const state = {
|
|
clients: [] as ReturnType<typeof makeClient>[],
|
|
consents: [] as Consent[],
|
|
auditLogsByCursor: undefined,
|
|
developerRequests: [],
|
|
};
|
|
await installDevApiMock(page, state);
|
|
|
|
await page.goto("/audit-logs");
|
|
await expect(
|
|
page.getByRole("heading", { name: /감사 로그|Audit Logs/ }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByText(
|
|
/감사 로그는 개발자 권한이 있어야 볼 수 있습니다|Audit logs are available only to users with developer access/i,
|
|
),
|
|
).toBeVisible();
|
|
const requestBtn = page.getByRole("button", {
|
|
name: /개발자 권한 신청/,
|
|
});
|
|
await expect(requestBtn).toBeVisible();
|
|
await requestBtn.click();
|
|
await expect(page).toHaveURL(/\/developer-requests$/);
|
|
await captureEvidence(page, testInfo, "security-user-audit-request-entry");
|
|
});
|
|
|
|
test("user with approved developer request can enter audit logs without CTA", async ({
|
|
page,
|
|
}, testInfo) => {
|
|
await seedAuth(page, "user");
|
|
|
|
const state = {
|
|
clients: [] as ReturnType<typeof makeClient>[],
|
|
consents: [] as Consent[],
|
|
auditLogs: [
|
|
{
|
|
event_id: "evt-audit-1",
|
|
timestamp: "2026-05-29T00:00:00.000Z",
|
|
user_id: "playwright-user",
|
|
event_type: "CLIENT_UPDATE",
|
|
status: "success" as const,
|
|
ip_address: "127.0.0.1",
|
|
user_agent: "playwright",
|
|
details: JSON.stringify({
|
|
action: "UPDATE_CLIENT",
|
|
target_id: "tenant-a-client",
|
|
tenant_id: "tenant-a",
|
|
}),
|
|
},
|
|
],
|
|
auditLogsByCursor: undefined,
|
|
developerRequests: [
|
|
{
|
|
id: "req-approved",
|
|
userId: "playwright-user",
|
|
userName: "Playwright User",
|
|
name: "Playwright User",
|
|
userEmail: "playwright@example.com",
|
|
organization: "Tenant A",
|
|
reason: "Need access",
|
|
status: "approved",
|
|
createdAt: "2026-05-29T00:00:00.000Z",
|
|
updatedAt: "2026-05-29T00:10:00.000Z",
|
|
approvedAt: "2026-05-29T00:10:00.000Z",
|
|
},
|
|
],
|
|
};
|
|
await installDevApiMock(page, state);
|
|
|
|
await page.goto("/audit-logs");
|
|
await expect(page.getByText("UPDATE_CLIENT")).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: /개발자 권한 신청/ }),
|
|
).toHaveCount(0);
|
|
await captureEvidence(page, testInfo, "security-user-audit-approved");
|
|
});
|
|
});
|