diff --git a/docs/analysis-mode.md b/docs/analysis-mode.md index a3d6fd7d3..50067707e 100644 --- a/docs/analysis-mode.md +++ b/docs/analysis-mode.md @@ -564,7 +564,55 @@ Analysis Mode 透過 `/_analysis` 路由提供三個 API,供前端 `framework_ | **CSV Formula Injection** | CSV 匯出時,值以 `=` / `+` / `-` / `@` 開頭者自動前置 tab,防止惡意公式在 Excel 中執行 | | **大量資料 DoS** | GroupBy 結果強制 `Take(10,001)` 截斷並標記 `Truncated: true`;載入上限 50,000 筆防 OOM | | **多租戶隔離** | 複用同一 `DC` 實例,EF Core global query filter 自動生效,租戶資料隔離無需額外處理 | -| **未授權存取** | `_AnalysisController` 標注 `[AllRights]`(需登入),Phase 2 評估欄位級權限 | +| **未授權存取** | `_AnalysisController` 標注 `[AllRights]`(需登入);VM 層 `[EnableAnalysis(AllowedRoles)]` 限制哪些角色可存取特定 VM;欄位層 `IAnalysisFieldPolicy` 過濾欄位可見性 | + +### 角色型存取控制(RBAC) + +Analysis Mode 提供兩層角色控制,角色識別符統一使用 **`RoleCode`**(非 `RoleName`)。 + +#### VM 層限制 + +```csharp +// 只有 RoleCode 為 "analyst" 或 "finance_mgr" 的使用者才能查詢此 VM +[EnableAnalysis(AllowedRoles = "analyst,finance_mgr")] +public class SalesListVM : BasePagedListVM { } +``` + +`AllowedRoles` 為空 = 不限角色(所有已登入使用者都可存取)。 + +#### 欄位層限制 + +```csharp +[Measure(AllowedFuncs = AggregateFunc.Sum, AllowedRoles = "finance_mgr")] +public decimal GrossProfit { get; set; } // 僅 finance_mgr 可見 + +[Dimension] // 無 AllowedRoles = 所有已授權使用者均可見 +public string Region { get; set; } +``` + +#### 自訂欄位策略 + +實作 `IAnalysisFieldPolicy` 可取代預設的 `AllowedRoles` 比對,實作更複雜的欄位過見邏輯(如依租戶動態決定): + +```csharp +public class MyFieldPolicy : IAnalysisFieldPolicy +{ + public IEnumerable Filter( + IEnumerable fields, ClaimsPrincipal user) + { + // ClaimsPrincipal 的 role claims 是以 RoleCode 建構 + var isAdmin = user.IsInRole("Admin"); + return fields.Where(f => + string.IsNullOrEmpty(f.AllowedRoles) || isAdmin || + f.AllowedRoles.Split(',').Any(r => user.IsInRole(r.Trim()))); + } +} + +// Program.cs +builder.Services.AddSingleton(); +``` + +> **重要**:`RoleCode = "Admin"` 的使用者永遠繞過欄位限制。`AllowedRoles` 填寫的值必須是 `FrameworkRole.RoleCode`,**不是** `RoleName`(顯示名稱)。 --- diff --git a/docs/dashboard-dev-guide.md b/docs/dashboard-dev-guide.md index 21cefb4cd..01b9c92c1 100644 --- a/docs/dashboard-dev-guide.md +++ b/docs/dashboard-dev-guide.md @@ -263,10 +263,27 @@ Set via `DashboardDefinition.Sharing`: // Public — visible to all authenticated users { "Mode": "public" } -// Role-based — visible to specific roles -{ "Mode": "private", "Roles": ["Manager", "Finance"] } +// Role-based — visible to specific roles (values must be RoleCode, not RoleName) +{ "Mode": "roles", "Roles": ["analyst", "finance_mgr"] } ``` +> **Important — RoleCode vs RoleName**: The `Roles` array must contain `RoleCode` values (the stable machine identifier stored in `FrameworkRole.RoleCode`), **not** `RoleName` (the human-readable display name that can be changed in the admin UI). Using `RoleName` would break access control whenever an admin renames a role. +> +> Example: if the role is displayed as "財務主管" in the admin UI but has `RoleCode = "finance_mgr"`, the JSON must use `"finance_mgr"`. + +#### Admin roles + +Admin role codes are configured via `DashboardOptions.AdminRoles` (defaults to `["Admin"]`): + +```csharp +builder.Services.Configure(opt => +{ + opt.AdminRoles = new[] { "Admin", "super_admin" }; // add additional admin RoleCodes +}); +``` + +Admin role users bypass all sharing restrictions and can view, edit, and delete any dashboard. + ### Multi-tenant isolation Dashboards are stored in tenant-specific subdirectories: diff --git a/docs/wtm-developer-manual.md b/docs/wtm-developer-manual.md index 717d6e06a..5304ba9cb 100644 --- a/docs/wtm-developer-manual.md +++ b/docs/wtm-developer-manual.md @@ -2200,13 +2200,99 @@ Analysis Mode 有多層安全防護: |------|------|------| | VM 白名單 | `AnalysisVmRegistry` | 只有標記 `[EnableAnalysis]` 的 ListVM 才能被查詢 | | 欄位白名單 | `AnalysisFieldScanner` | 只有標記 `[Dimension]`/`[Measure]` 的欄位才能作為維度/度量 | -| 欄位存取控制 | `IAnalysisFieldPolicy` | 可依角色過濾可用欄位(例如非管理員看不到薪資) | +| VM 層角色控制 | `[EnableAnalysis(AllowedRoles = "...")]` | 限制可存取此 VM 的角色(值為 **RoleCode**,逗號分隔) | +| 欄位層角色控制 | `[Dimension/Measure(AllowedRoles = "...")]` | 限制可見特定欄位的角色(值同為 **RoleCode**) | +| 欄位存取策略 | `IAnalysisFieldPolicy` | 框架呼叫 `Filter(fields, ClaimsPrincipal)` 過濾可用欄位;`ClaimsPrincipal` 以 `RoleCode` 建構 role claims | | 資料權限 | `DPWhere` | `GetSearchQuery()` 中的資料權限在 Analysis 中同樣生效 | | Expression 防注入 | 白名單比對 | 欄位名稱必須完全匹配白名單,無法注入任意表達式 | | 結果截斷 | `Take(50_000)` + 10,000 行 | 防止大量資料洩露 | | CSV 防公式注入 | 前置 tab | `=+@-\t\r` 開頭的儲存格加 tab | -### 10.5 審計日誌(ActionLog) +**角色識別符規則**:`AllowedRoles` 填入的值必須是 `RoleCode`(資料庫 `FrameworkRole.RoleCode` 欄位),**不是** `RoleName`。`RoleCode` 是穩定的機器識別符;`RoleName` 可能在 UI 中被修改。 + +```csharp +// ✅ 正確:使用 RoleCode +[EnableAnalysis(AllowedRoles = "analyst,finance_mgr")] +public class SalesListVM : BasePagedListVM + +[Measure(AllowedRoles = "finance_mgr")] +public decimal Salary { get; set; } + +// ❌ 錯誤:不要使用 RoleName(顯示名稱) +[EnableAnalysis(AllowedRoles = "財務主管")] // RoleName 可變,不應依賴 +``` + +`RoleCode = "Admin"` 的使用者永遠繞過欄位層限制(完整存取)。 + +### 10.5 跨模組 RBAC 統一規範 + +WTM 三個進階模組(Analysis、Dashboard、ETL)使用相同的角色識別符(`RoleCode`),但各自有不同的設定層次: + +| 模組 | RBAC 設定位置 | 角色識別符 | Admin 快速路徑 | +|------|--------------|-----------|--------------| +| **Analysis** | `[EnableAnalysis(AllowedRoles)]`(VM 層) + `[Dimension/Measure(AllowedRoles)]`(欄位層) | `RoleCode` | `RoleCode = "Admin"` 繞過所有限制 | +| **Dashboard** | `DashboardDefinition.Sharing.Roles`(分享設定,陣列) | `RoleCode` | `DashboardOptions.AdminRoles`(預設 `["Admin"]`)可存取全部 Dashboard | +| **ETL** | WTM 標準 `[ActionDescription]` + Admin 後台功能權限授予 | `FunctionPrivilege`(URL 為鍵) | 同一般 Controller 機制 | + +#### Analysis RBAC 設定範例 + +```csharp +// VM 層:限制哪些角色可以查詢這個 VM +[EnableAnalysis(AllowedRoles = "analyst,bi_viewer")] +[ActionDescription("銷售分析")] +public class SalesListVM : BasePagedListVM +{ + // 欄位層:限制哪些角色可見此欄位 + [Measure(AllowedFuncs = AggregateFunc.Sum, AllowedRoles = "finance_mgr")] + public decimal GrossProfit { get; set; } + + [Dimension] // 無 AllowedRoles = 所有已授權使用者都能看到 + public string Region { get; set; } +} +``` + +#### Dashboard RBAC 設定範例 + +```json +{ + "id": "sales-overview", + "title": "Sales Overview", + "owner": "alice", + "sharing": { + "mode": "roles", + "roles": ["analyst", "sales_mgr"] + } +} +``` + +- `mode: "private"` — 僅 owner 可見 +- `mode: "public"` — 所有已登入使用者可見 +- `mode: "roles"` — `roles` 陣列中列出的 `RoleCode` 可見(加上 owner 和 Admin) + +`roles` 陣列的值必須是 `RoleCode`,與 Analysis 的 `AllowedRoles` 規則一致。 + +#### ETL RBAC + +ETL Controller 使用標準 WTM `[AllRights]`(已登入即可)和 `[ActionDescription]`(每個 action 一條 URL 功能權限)。透過 Admin 後台的「角色管理 → 功能授權」頁面為角色授予或撤銷各個 ETL 操作的存取權限,不需要修改程式碼。 + +``` +_EtlController.Index → URL: /_etl/index → 在 Admin 後台為角色 "etl_operator" 授權 +_EtlController.Trigger → URL: /_etl/trigger/{id} +_EtlController.Pause → URL: /_etl/pause/{id} +``` + +#### 不得混用 RoleCode 與 RoleName + +所有三個模組的角色設定都必須使用 `RoleCode`,禁止使用 `RoleName`: + +| ❌ 錯誤 | ✅ 正確 | +|--------|--------| +| `AllowedRoles = "財務主管"` | `AllowedRoles = "finance_mgr"` | +| `roles: ["業務分析師"]` | `roles: ["analyst"]` | + +`RoleName` 是 UI 顯示名稱,由管理員可以在後台修改;`RoleCode` 是不可變的機器識別符,是安全邊界的正確依據。 + +### 10.6 審計日誌(ActionLog) 所有 Controller Action(除標記 `[NoLog]` 者)自動寫入 `ActionLog` 表: