1
0
forked from baron/baron-sso

adminfront: TenantListPage에 세부 기능 권한(tenants / manage_tenants) 우회 및 제어 전격 적용하여 접근 제한 버그 해결

This commit is contained in:
2026-06-12 15:50:46 +09:00
parent d39838a1c9
commit 2820ca941d

View File

@@ -377,6 +377,7 @@ function TenantListPage() {
queryFn: fetchMe,
});
const profileRole = normalizeAdminRole(profile?.role);
const isWritable = profileRole === "super_admin" || !!profile?.systemPermissions?.manage_tenants;
const query = useInfiniteQuery({
queryKey: ["tenants", "lazy", debouncedSearch, scopeTenantId],
@@ -581,7 +582,7 @@ function TenantListPage() {
return () => window.removeEventListener("message", onMessage);
}, [allTenants, scopePickerOpen]);
if (profile && profileRole !== "super_admin") {
if (profile && profileRole !== "super_admin" && !profile?.systemPermissions?.tenants) {
return (
<div className="flex h-[50vh] flex-col items-center justify-center space-y-4">
<h3 className="text-lg font-bold">
@@ -840,81 +841,83 @@ function TenantListPage() {
}
actions={
<>
<RoleGuard roles={["super_admin"]}>
<input
ref={fileInputRef}
name="tenant-import-file"
type="file"
accept=".csv,text/csv"
className="hidden"
data-testid="tenant-import-input"
onChange={handleImportFile}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
data-testid="tenant-data-mgmt-btn"
className="gap-2 h-9"
>
<LayoutDashboard size={16} />
{t("ui.admin.tenants.data_mgmt", "데이터 관리")}
<ChevronDown size={14} className="opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem
onClick={handleTemplateDownload}
data-testid="tenant-template-menu-item"
className="cursor-pointer"
>
<FileSpreadsheet
size={16}
className="mr-2 opacity-50"
/>
{t(
"ui.admin.tenants.csv_template",
"템플릿 다운로드",
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => fileInputRef.current?.click()}
disabled={importMutation.isPending}
data-testid="tenant-import-menu-item"
className="cursor-pointer"
>
<Upload size={16} className="mr-2 opacity-50" />
{t("ui.admin.tenants.import", "CSV 가져오기")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => exportMutation.mutate(false)}
disabled={exportMutation.isPending}
data-testid="tenant-export-menu-item"
className="cursor-pointer"
>
<Download size={16} className="mr-2 opacity-50" />
{t(
"ui.admin.tenants.export_without_ids",
"UUID 제외 내보내기",
)}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => exportMutation.mutate(true)}
disabled={exportMutation.isPending}
data-testid="tenant-export-with-ids-menu-item"
className="cursor-pointer"
>
<Download size={16} className="mr-2 opacity-50" />
{t(
"ui.admin.tenants.export_with_ids",
"UUID 포함 내보내기",
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</RoleGuard>
{isWritable && (
<>
<input
ref={fileInputRef}
name="tenant-import-file"
type="file"
accept=".csv,text/csv"
className="hidden"
data-testid="tenant-import-input"
onChange={handleImportFile}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
data-testid="tenant-data-mgmt-btn"
className="gap-2 h-9"
>
<LayoutDashboard size={16} />
{t("ui.admin.tenants.data_mgmt", "데이터 관리")}
<ChevronDown size={14} className="opacity-50" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem
onClick={handleTemplateDownload}
data-testid="tenant-template-menu-item"
className="cursor-pointer"
>
<FileSpreadsheet
size={16}
className="mr-2 opacity-50"
/>
{t(
"ui.admin.tenants.csv_template",
"템플릿 다운로드",
)}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => fileInputRef.current?.click()}
disabled={importMutation.isPending}
data-testid="tenant-import-menu-item"
className="cursor-pointer"
>
<Upload size={16} className="mr-2 opacity-50" />
{t("ui.admin.tenants.import", "CSV 가져오기")}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => exportMutation.mutate(false)}
disabled={exportMutation.isPending}
data-testid="tenant-export-menu-item"
className="cursor-pointer"
>
<Download size={16} className="mr-2 opacity-50" />
{t(
"ui.admin.tenants.export_without_ids",
"UUID 제외 내보내기",
)}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => exportMutation.mutate(true)}
disabled={exportMutation.isPending}
data-testid="tenant-export-with-ids-menu-item"
className="cursor-pointer"
>
<Download size={16} className="mr-2 opacity-50" />
{t(
"ui.admin.tenants.export_with_ids",
"UUID 포함 내보내기",
)}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
)}
<Button
variant="outline"
@@ -928,14 +931,14 @@ function TenantListPage() {
{t("ui.common.refresh", "새로고침")}
</span>
</Button>
<RoleGuard roles={["super_admin"]}>
{isWritable && (
<Button asChild size="sm" className="h-9">
<Link to="/tenants/new">
<Plus size={16} />
{t("ui.admin.tenants.add", "테넌트 추가")}
</Link>
</Button>
</RoleGuard>
)}
</>
}
/>
@@ -1071,7 +1074,7 @@ function TenantListPage() {
{t("ui.common.apply", "적용")}
</Button>
<div className="w-px h-4 bg-background/20 mx-1" />
<RoleGuard roles={["super_admin"]}>
{isWritable && (
<Button
variant="ghost"
size="sm"
@@ -1083,7 +1086,7 @@ function TenantListPage() {
<Trash2 size={14} />
{t("ui.common.delete", "삭제")}
</Button>
</RoleGuard>
)}
</div>
<Button
variant="ghost"