1
0
forked from baron/baron-sso

custom claim 권한체크 확인

This commit is contained in:
2026-06-11 08:29:25 +09:00
parent 839ca9d407
commit 4d77060b5d
79 changed files with 4268 additions and 670 deletions

View File

@@ -50,6 +50,43 @@ test("clients page loads correctly", async ({ page }) => {
).toBeVisible();
});
test("clients page shows Tenant-limited only for tenant access restricted RP", async ({
page,
}) => {
await seedAuth(page, "super_admin");
await installDevApiMock(page, {
clients: [
makeClient("client-limited", {
name: "Limited RP",
createdAt: "2026-05-02T00:00:00.000Z",
metadata: {
tenant_access_restricted: true,
allowed_tenants: ["tenant-1"],
},
}),
makeClient("client-open", {
name: "Open RP",
createdAt: "2026-05-01T00:00:00.000Z",
metadata: {
tenant_access_restricted: false,
allowed_tenants: [],
},
}),
],
consents: [] as Consent[],
auditLogsByCursor: undefined,
});
await page.goto("/clients");
const limitedRow = page.locator("tbody tr", { hasText: "Limited RP" });
await expect(limitedRow).toContainText("Tenant-limited");
const openRow = page.locator("tbody tr", { hasText: "Open RP" });
await expect(openRow).not.toContainText("Tenant-limited");
await expect(page.getByText("Tenant-scoped")).toHaveCount(0);
});
test("overview page shows recent RP changes", async ({ page }) => {
await seedAuth(page, "super_admin");
await installDevApiMock(page, {

View File

@@ -0,0 +1,63 @@
import { expect, test } from "@playwright/test";
import {
type Consent,
installDevApiMock,
makeClient,
seedAuth,
} from "./helpers/devfront-fixtures";
import { installDevFrontStaticRoutes } from "./helpers/static-devfront";
test.describe("DevFront RP claim cache", () => {
test.beforeEach(async ({ page }) => {
await installDevFrontStaticRoutes(page);
await seedAuth(page, "super_admin");
});
test("keeps saved RP claim value visible after saving", async ({ page }) => {
const state = {
clients: [
makeClient("client-claims", {
name: "Claims app",
metadata: {
id_token_claims: [
{
namespace: "rp_claims",
key: "old_claim",
value: "A",
valueType: "text",
readPermission: "admin_only",
writePermission: "admin_only",
},
],
},
}),
],
consents: [] as Consent[],
auditLogsByCursor: undefined,
mockRole: "super_admin",
};
await installDevApiMock(page, state);
await page.goto("http://devfront.test/clients/client-claims/settings");
const claimKeyInput = page
.getByPlaceholder(/e\.g\. locale|예: locale/i)
.first();
await expect(claimKeyInput).toHaveValue("old_claim");
await claimKeyInput.fill("new_claim");
await page.getByRole("button", { name: /^저장$|^Save$/i }).click();
await expect
.poll(
() =>
(
state.clients[0]?.metadata?.id_token_claims as
| Array<{ key?: string }>
| undefined
)?.[0]?.key,
)
.toBe("new_claim");
await expect(claimKeyInput).toHaveValue("new_claim");
});
});

View File

@@ -155,14 +155,21 @@ test.describe("DevFront clients lifecycle", () => {
.getByLabel(/Claim value type|Claim 값 타입/i)
.first()
.selectOption("date");
await expect(
page.getByRole("columnheader", { name: /Default Value|기본값/i }),
).toBeVisible();
await page
.getByPlaceholder(/Claim 값을 입력하세요|Enter the claim value/i)
.getByPlaceholder(/기본값을 입력하세요|Enter the default value/i)
.first()
.fill("2026-06-09");
await page
.getByLabel(/읽기 권한|Read permission/i)
.getByLabel(/Nullable|Null 허용/i)
.first()
.selectOption("user_and_admin");
.click();
await page
.getByLabel(/Read 사용자 허용|Read user allowed/i)
.first()
.click();
await page.getByRole("button", { name: /Claim 추가|Add Claim/i }).click();
await page
@@ -174,7 +181,7 @@ test.describe("DevFront clients lifecycle", () => {
.nth(1)
.selectOption("number");
await page
.getByPlaceholder(/Claim 값을 입력하세요|Enter the claim value/i)
.getByPlaceholder(/기본값을 입력하세요|Enter the default value/i)
.nth(1)
.fill("2");
@@ -238,6 +245,7 @@ test.describe("DevFront clients lifecycle", () => {
key?: string;
value?: string;
valueType?: string;
nullable?: boolean;
readPermission?: string;
writePermission?: string;
}>
@@ -245,6 +253,18 @@ test.describe("DevFront clients lifecycle", () => {
)?.[0]?.valueType,
)
.toBe("date");
await expect
.poll(
() =>
(
state.clients[0]?.metadata?.id_token_claims as
| Array<{
nullable?: boolean;
}>
| undefined
)?.[0]?.nullable,
)
.toBe(true);
await expect
.poll(
() =>
@@ -313,18 +333,25 @@ test.describe("DevFront clients lifecycle", () => {
page.getByPlaceholder(/e\.g\. locale|예: locale/i).nth(1),
).toHaveValue("tier");
await expect(
page.getByPlaceholder(/Claim 값을 입력하세요|Enter the claim value/i),
page.getByPlaceholder(/기본값을 입력하세요|Enter the default value/i),
).toHaveCount(2);
await expect(
page
.getByPlaceholder(/Claim 값을 입력하세요|Enter the claim value/i)
.getByPlaceholder(/기본값을 입력하세요|Enter the default value/i)
.first(),
).toHaveValue("2026-06-09");
await expect(
page
.getByPlaceholder(/Claim 값을 입력하세요|Enter the claim value/i)
.getByPlaceholder(/기본값을 입력하세요|Enter the default value/i)
.nth(1),
).toHaveValue("2");
await expect(page.getByLabel(/Nullable|Null 허용/i).first()).toBeChecked();
await expect(
page.getByLabel(/Read 사용자 허용|Read user allowed/i).first(),
).toBeChecked();
await expect(
page.getByLabel(/Write 사용자 허용|Write user allowed/i).first(),
).not.toBeChecked();
});
test("headless login uses jwks uri only and shows cache actions", async ({

View File

@@ -40,6 +40,18 @@ test.describe("DevFront consents", () => {
valueType: "datetime",
value: "2026-06-09T09:30",
},
{
namespace: "rp_claims",
key: "active_member",
valueType: "boolean",
value: "true",
},
{
namespace: "rp_claims",
key: "score",
valueType: "number",
value: "1",
},
],
},
}),
@@ -78,9 +90,14 @@ test.describe("DevFront consents", () => {
await expect(page.getByText("RP Custom Claims")).toBeVisible();
await expect(page.getByText("contract_date")).toBeVisible();
await expect(page.getByText("approved_at")).toBeVisible();
await expect(page.getByText("active_member")).toBeVisible();
await expect(page.locator('input[type="date"]')).toHaveValue("2026-06-09");
await page.locator('input[type="date"]').fill("2026-06-10");
await page.locator('input[type="datetime-local"]').fill("2026-06-09T10:30");
await page
.getByLabel(/active_member.*boolean|boolean.*active_member/i)
.selectOption("false");
await page.getByLabel(/score.*number|number.*score/i).fill("42");
await page
.getByLabel(/쓰기 권한|Write permission/i)
.first()
@@ -92,6 +109,10 @@ test.describe("DevFront consents", () => {
await expect
.poll(() => state.consents[0]?.rpMetadata?.approved_at)
.toBe("2026-06-09T10:30");
await expect
.poll(() => state.consents[0]?.rpMetadata?.active_member)
.toBe(false);
await expect.poll(() => state.consents[0]?.rpMetadata?.score).toBe(42);
await expect
.poll(
() =>

View File

@@ -466,6 +466,7 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
createdAt: client.createdAt,
redirectUris: client.redirectUris,
scopes: client.scopes,
metadata: client.metadata ?? {},
})),
limit: 50,
offset: 0,
@@ -612,6 +613,7 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
token: "https://issuer/oauth2/token",
userinfo: "https://issuer/userinfo",
},
headlessJwksCache: found.headlessJwksCache,
});
}
@@ -635,6 +637,7 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
token: "https://issuer/oauth2/token",
userinfo: "https://issuer/userinfo",
},
headlessJwksCache: found.headlessJwksCache,
});
}
@@ -720,6 +723,7 @@ export async function installDevApiMock(page: Page, state: DevApiMockState) {
token: "https://issuer/oauth2/token",
userinfo: "https://issuer/userinfo",
},
headlessJwksCache: found.headlessJwksCache,
});
}

View File

@@ -0,0 +1,93 @@
import { readFile, stat } from "node:fs/promises";
import { extname, join, normalize, resolve } from "node:path";
import type { Page } from "@playwright/test";
const contentTypes: Record<string, string> = {
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".ico": "image/x-icon",
".js": "application/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".map": "application/json; charset=utf-8",
".mjs": "application/javascript; charset=utf-8",
".png": "image/png",
".svg": "image/svg+xml",
".txt": "text/plain; charset=utf-8",
".webp": "image/webp",
".woff": "font/woff",
".woff2": "font/woff2",
};
function safeDistPath(distDir: string, pathname: string) {
const decoded = decodeURIComponent(pathname);
const relative = decoded.replace(/^\/+/, "");
const safe = normalize(relative).replace(/^(\.\.(?:[\\/]|$))+/, "");
return join(distDir, safe);
}
async function resolveStaticFile(distDir: string, pathname: string) {
const indexPath = join(distDir, "index.html");
let filePath = safeDistPath(
distDir,
pathname === "/" ? "/index.html" : pathname,
);
try {
const fileStat = await stat(filePath);
if (fileStat.isDirectory()) {
filePath = join(filePath, "index.html");
}
} catch {
filePath = indexPath;
}
try {
return {
body: await readFile(filePath),
contentType:
contentTypes[extname(filePath).toLowerCase()] ??
"application/octet-stream",
};
} catch {
return null;
}
}
export async function installDevFrontStaticRoutes(
page: Page,
options: {
distDir?: string;
origin?: string;
} = {},
) {
const origin = options.origin ?? "http://devfront.test";
const distDir = resolve(
options.distDir ??
process.env.DEVFRONT_DIST_DIR ??
"/tmp/baron-sso-devfront-dist",
);
await page.route(`${origin}/**`, async (route) => {
const url = new URL(route.request().url());
if (url.pathname === "/api" || url.pathname.startsWith("/api/")) {
await route.fallback();
return;
}
const file = await resolveStaticFile(distDir, url.pathname);
if (!file) {
await route.fulfill({
status: 500,
contentType: "application/json; charset=utf-8",
body: JSON.stringify({ error: "devfront_dist_not_found" }),
});
return;
}
await route.fulfill({
status: 200,
contentType: file.contentType,
body: file.body,
});
});
}