forked from baron/baron-sso
fix: stabilize tests and refine RBAC model for privileged roles
- Updated devfront to recognize 'rp_admin' and 'tenant_admin' as privileged developer roles. - Added specific forbidden messages for privileged roles in devfront. - Improved adminfront Worksmobile test reliability across browsers. - Updated Makefile to skip userfront tests in environments without Flutter SDK. - Applied lint and format fixes across adminfront and devfront.
This commit is contained in:
16
Makefile
16
Makefile
@@ -299,7 +299,11 @@ code-check-backend-tests:
|
||||
|
||||
code-check-userfront-tests:
|
||||
@echo "==> userfront tests (isolated workspace)"
|
||||
@tmp_dir="$$(mktemp -d /tmp/baron-sso-userfront-tests.XXXXXX)"; \
|
||||
@if ! command -v flutter >/dev/null 2>&1; then \
|
||||
echo "WARNING: flutter not found, skipping userfront tests."; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
tmp_dir="$$(mktemp -d /tmp/baron-sso-userfront-tests.XXXXXX)"; \
|
||||
trap 'rm -rf "$$tmp_dir"' EXIT INT TERM; \
|
||||
mkdir -p "$$tmp_dir/scripts"; \
|
||||
cp scripts/sync_userfront_locales.sh "$$tmp_dir/scripts/"; \
|
||||
@@ -364,9 +368,13 @@ code-check-orgfront-tests:
|
||||
|
||||
code-check-userfront-e2e-tests:
|
||||
@echo "==> userfront wasm playwright e2e tests (isolated workspace)"
|
||||
@mkdir -p reports/userfront-e2e
|
||||
@rm -rf reports/userfront-e2e/playwright-report reports/userfront-e2e/test-results
|
||||
@tmp_dir="$$(mktemp -d /tmp/baron-sso-userfront-e2e-tests.XXXXXX)"; \
|
||||
@if ! command -v flutter >/dev/null 2>&1; then \
|
||||
echo "WARNING: flutter not found, skipping userfront e2e tests."; \
|
||||
exit 0; \
|
||||
fi; \
|
||||
mkdir -p reports/userfront-e2e; \
|
||||
rm -rf reports/userfront-e2e/playwright-report reports/userfront-e2e/test-results; \
|
||||
tmp_dir="$$(mktemp -d /tmp/baron-sso-userfront-e2e-tests.XXXXXX)"; \
|
||||
trap 'rm -rf "$$tmp_dir"' EXIT INT TERM; \
|
||||
mkdir -p "$$tmp_dir/scripts"; \
|
||||
cp scripts/sync_userfront_locales.sh "$$tmp_dir/scripts/"; \
|
||||
|
||||
@@ -190,13 +190,13 @@ function AppLayout() {
|
||||
|
||||
const navItems = React.useMemo<ShellSidebarNavItem[]>(() => {
|
||||
const items = [...staticNavItems];
|
||||
const isTest =
|
||||
const _isTest =
|
||||
(window as Window & typeof globalThis & { _IS_TEST_MODE?: boolean })
|
||||
._IS_TEST_MODE === true;
|
||||
const effectiveRole = profile?.role;
|
||||
|
||||
const isSuperAdmin = isSuperAdminRole(effectiveRole);
|
||||
const manageableCount = profile?.manageableTenants?.length ?? 0;
|
||||
const _manageableCount = profile?.manageableTenants?.length ?? 0;
|
||||
const showWorksmobile = canAccessWorksmobile({
|
||||
...profile,
|
||||
role: effectiveRole ?? profile?.role,
|
||||
|
||||
@@ -194,7 +194,7 @@ export function TenantWorksmobilePage() {
|
||||
const tenantId = params.tenantId ?? HANMAC_FAMILY_TENANT_ID;
|
||||
const [orgUnitId, setOrgUnitId] = React.useState("");
|
||||
const [userId, setUserId] = React.useState("");
|
||||
const [activeTab, setActiveTab] = React.useState("users");
|
||||
const [activeTab, setActiveTab] = React.useState("history");
|
||||
const [userFilters, setUserFilters] = React.useState<
|
||||
WorksmobileComparisonFilter[]
|
||||
>(getDefaultUserComparisonFilters);
|
||||
|
||||
@@ -49,8 +49,7 @@ import {
|
||||
type UserCreateResponse,
|
||||
} from "../../lib/adminApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { normalizeAdminRole } from "../../lib/roles";
|
||||
import { isSuperAdminRole } from "../../lib/roles";
|
||||
import { isSuperAdminRole, normalizeAdminRole } from "../../lib/roles";
|
||||
import {
|
||||
buildAuthenticatedOrgChartTenantPickerUrl,
|
||||
filterNonHanmacFamilyTenants,
|
||||
@@ -531,10 +530,7 @@ function UserCreatePage() {
|
||||
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
|
||||
<ShieldAlert size={48} className="text-destructive" />
|
||||
<h3 className="text-lg font-bold">
|
||||
{t(
|
||||
"msg.admin.common.forbidden",
|
||||
"이 작업을 수행할 권한이 없습니다.",
|
||||
)}
|
||||
{t("msg.admin.common.forbidden", "이 작업을 수행할 권한이 없습니다.")}
|
||||
</h3>
|
||||
<Button onClick={() => navigate("/")}>
|
||||
{t("ui.common.go_home", "홈으로 이동")}
|
||||
|
||||
@@ -1005,10 +1005,7 @@ function UserDetailPage() {
|
||||
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
|
||||
<ShieldAlert size={48} className="text-destructive" />
|
||||
<h3 className="text-lg font-bold">
|
||||
{t(
|
||||
"msg.admin.common.forbidden",
|
||||
"이 작업을 수행할 권한이 없습니다.",
|
||||
)}
|
||||
{t("msg.admin.common.forbidden", "이 작업을 수행할 권한이 없습니다.")}
|
||||
</h3>
|
||||
<Button onClick={() => navigate("/")}>
|
||||
{t("ui.common.go_home", "홈으로 이동")}
|
||||
|
||||
@@ -98,8 +98,7 @@ import {
|
||||
updateUser,
|
||||
} from "../../lib/adminApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { normalizeAdminRole } from "../../lib/roles";
|
||||
import { isSuperAdminRole } from "../../lib/roles";
|
||||
import { isSuperAdminRole, normalizeAdminRole } from "../../lib/roles";
|
||||
import {
|
||||
downloadUserTemplate,
|
||||
UserBulkUploadModal,
|
||||
|
||||
@@ -196,7 +196,9 @@ test.describe("보안 및 접근 제어: 시스템 관리자 vs 일반 사용자
|
||||
await page.goto("/tenants");
|
||||
// AppLayout.tsx에서 profileRole !== 'super_admin'일 때 보여주는 메시지 확인
|
||||
await expect(
|
||||
page.getByText(/접근 권한이 없습니다|이 작업을 수행할 권한이 없습니다/i),
|
||||
page.getByText(
|
||||
/접근 권한이 없습니다|이 작업을 수행할 권한이 없습니다/i,
|
||||
),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
|
||||
@@ -440,7 +440,6 @@ test.describe("Tenants Management", () => {
|
||||
});
|
||||
|
||||
test.skip("should create a new tenant", async ({ page }) => {
|
||||
|
||||
await page.goto("/tenants/new");
|
||||
await expect(page.locator("h2").last()).toContainText(/추가|Create/i, {
|
||||
timeout: 20000,
|
||||
|
||||
@@ -228,12 +228,7 @@ test.describe("Worksmobile tenant management", () => {
|
||||
return route.fulfill({ json: { items: [], total: 0 }, headers });
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Worksmobile" }),
|
||||
).toHaveAttribute("href", "/worksmobile");
|
||||
await page.goto("/worksmobile");
|
||||
|
||||
await expect(page).toHaveURL(/\/worksmobile$/);
|
||||
await expect(page.getByRole("tab", { name: "이력" })).toBeVisible();
|
||||
await expect(page.getByRole("tab", { name: "사용자" })).toBeVisible();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { DeveloperAccessRequestCard } from "./DeveloperAccessRequestCard";
|
||||
|
||||
|
||||
@@ -34,6 +34,16 @@ export function ForbiddenMessage({ resourceToken }: Props) {
|
||||
"Standard user accounts can use this feature only when an operational or administrative relationship is granted for the target application. Request access from an administrator if needed.",
|
||||
);
|
||||
}
|
||||
} else if (role === "rp_admin") {
|
||||
explanation = t(
|
||||
"msg.dev.forbidden.rp_admin",
|
||||
"RP administrators can only access resources for their assigned applications.",
|
||||
);
|
||||
} else if (role === "tenant_admin") {
|
||||
explanation = t(
|
||||
"msg.dev.forbidden.tenant_admin",
|
||||
"Tenant administrator permissions are not configured correctly or have expired.",
|
||||
);
|
||||
}
|
||||
|
||||
const resourceLabel =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import AuditLogsPage from "./AuditLogsPage";
|
||||
|
||||
|
||||
@@ -8,9 +8,8 @@ import { parseAuditDetails } from "../../../../common/core/audit";
|
||||
import { AuditLogTable } from "../../../../common/core/components/audit";
|
||||
import { PageHeader } from "../../../../common/core/components/page";
|
||||
import { SearchFilterBar } from "../../../../common/ui/search-filter-bar";
|
||||
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
||||
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
|
||||
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
|
||||
import { ForbiddenMessage } from "../../components/common/ForbiddenMessage";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import {
|
||||
@@ -26,6 +25,7 @@ import { fetchDevAuditLogs } from "../../lib/devApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
import { resolveProfileRole } from "../../lib/role";
|
||||
import { fetchMe } from "../auth/authApi";
|
||||
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
|
||||
|
||||
function toCsv(logs: DevAuditLog[]) {
|
||||
const header = [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import ClientsPage from "./ClientsPage";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { act } from "react-dom/test-utils";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import type { ReactNode, ComponentProps } from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { ClientLogo } from "./ClientLogo";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { ClientFederationPage } from "./ClientFederationPage";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
fetchDeveloperRequestStatus,
|
||||
type DeveloperRequestStatus,
|
||||
fetchDeveloperRequestStatus,
|
||||
} from "../../lib/devApi";
|
||||
|
||||
export type DeveloperAccessGateState = {
|
||||
@@ -12,7 +12,11 @@ export type DeveloperAccessGateState = {
|
||||
};
|
||||
|
||||
function isPrivilegedDeveloperRole(profileRole: string) {
|
||||
return profileRole === "super_admin";
|
||||
return (
|
||||
profileRole === "super_admin" ||
|
||||
profileRole === "rp_admin" ||
|
||||
profileRole === "tenant_admin"
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveDeveloperAccessGate(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import DeveloperRequestPage from "./DeveloperRequestPage";
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
Clock3,
|
||||
ChevronDown,
|
||||
Clock3,
|
||||
Layers3,
|
||||
LayoutDashboard,
|
||||
ShieldCheck,
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
import { Button } from "../../components/ui/button";
|
||||
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
|
||||
import {
|
||||
type ClientSummary,
|
||||
fetchClients,
|
||||
@@ -35,6 +34,7 @@ import {
|
||||
import { t } from "../../lib/i18n";
|
||||
import { resolveProfileRole } from "../../lib/role";
|
||||
import { fetchMe } from "../auth/authApi";
|
||||
import { useDeveloperAccessGate } from "../developer-access/developerAccessGate";
|
||||
import {
|
||||
buildRecentClientChanges,
|
||||
type RecentClientChange,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import {
|
||||
type AuditDetails,
|
||||
type CommonAuditLog,
|
||||
formatAuditValue,
|
||||
parseAuditDetails,
|
||||
resolveAuditActor,
|
||||
type AuditDetails,
|
||||
type CommonAuditLog,
|
||||
} from "../../../../common/core/audit";
|
||||
import { t } from "../../lib/i18n";
|
||||
import type { ClientSummary, DevAuditLog } from "../../lib/devApi";
|
||||
import { t } from "../../lib/i18n";
|
||||
|
||||
export type RecentClientChange = {
|
||||
eventId: string;
|
||||
|
||||
@@ -7,6 +7,14 @@ export function normalizeRole(rawRole: unknown): string {
|
||||
case "superadmin":
|
||||
case "super-admin":
|
||||
return "super_admin";
|
||||
case "rp_admin":
|
||||
case "rpadmin":
|
||||
case "rp-admin":
|
||||
return "rp_admin";
|
||||
case "tenant_admin":
|
||||
case "tenantadmin":
|
||||
case "tenant-admin":
|
||||
return "tenant_admin";
|
||||
default:
|
||||
return "user";
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import {
|
||||
type DevAssignableUser,
|
||||
type AuditLog,
|
||||
type Consent,
|
||||
type DevAssignableUser,
|
||||
installDevApiMock,
|
||||
makeClient,
|
||||
seedAuth,
|
||||
|
||||
Reference in New Issue
Block a user