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