From 7dff319882b433e4216f95a24a41b31ad7dff162 Mon Sep 17 00:00:00 2001 From: cct0831 Date: Sat, 21 Mar 2026 08:03:35 +0800 Subject: [PATCH] fix(analysis): enforce vm access checks on saved query endpoints --- .../_AnalysisController.cs | 16 +++++- .../Analysis/AnalysisControllerTests.cs | 54 +++++++++++++++++++ 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/WalkingTec.Mvvm.Mvc/_AnalysisController.cs b/src/WalkingTec.Mvvm.Mvc/_AnalysisController.cs index 0e111330..7c6f6d00 100644 --- a/src/WalkingTec.Mvvm.Mvc/_AnalysisController.cs +++ b/src/WalkingTec.Mvvm.Mvc/_AnalysisController.cs @@ -304,6 +304,12 @@ public IActionResult ListSavedQueries([FromQuery] string listVmType) if (string.IsNullOrWhiteSpace(listVmType)) return BadRequest("listVmType is required."); + Type vmType; + try { vmType = _registry.Resolve(listVmType); } + catch (AnalysisVmNotFoundException ex) { return NotFound(ex.Message); } + catch (InvalidOperationException ex) { return BadRequest(ex.Message); } + if (!CheckAccess(vmType)) return Forbid(); + var userCode = Wtm?.LoginUserInfo?.ITCode ?? string.Empty; var rows = Wtm!.DC.Set() @@ -337,8 +343,11 @@ public IActionResult SaveQuery([FromBody] SaveQueryRequest? req) if (string.IsNullOrWhiteSpace(req.Name)) return BadRequest("查詢名稱不可為空。"); if (string.IsNullOrWhiteSpace(req.Config?.ListVmType)) return BadRequest("Config.ListVmType is required."); - try { _registry.Resolve(req.Config.ListVmType); } + Type vmType; + try { vmType = _registry.Resolve(req.Config.ListVmType); } catch (AnalysisVmNotFoundException ex) { return NotFound(ex.Message); } + catch (InvalidOperationException ex) { return BadRequest(ex.Message); } + if (!CheckAccess(vmType)) return Forbid(); var userCode = Wtm?.LoginUserInfo?.ITCode ?? string.Empty; var configJson = JsonSerializer.Serialize(req.Config, _camelCase); @@ -385,6 +394,11 @@ public IActionResult GetSavedQuery(Guid id) catch (JsonException) { return BadRequest("儲存的查詢格式無效。"); } if (config == null) return BadRequest("儲存的查詢格式無效。"); + Type vmType; + try { vmType = _registry.Resolve(config.ListVmType); } + catch (AnalysisVmNotFoundException ex) { return NotFound(ex.Message); } + catch (InvalidOperationException ex) { return BadRequest(ex.Message); } + if (!CheckAccess(vmType)) return Forbid(); return new JsonResult(config, _camelCase); } diff --git a/test/WalkingTec.Mvvm.Core.Test/Analysis/AnalysisControllerTests.cs b/test/WalkingTec.Mvvm.Core.Test/Analysis/AnalysisControllerTests.cs index 6b3d8f5f..2cf9528e 100644 --- a/test/WalkingTec.Mvvm.Core.Test/Analysis/AnalysisControllerTests.cs +++ b/test/WalkingTec.Mvvm.Core.Test/Analysis/AnalysisControllerTests.cs @@ -2496,6 +2496,36 @@ public void ListSavedQueries_returns_public_and_owned_queries() Assert.IsTrue(names.Contains("B_Public"), "UserA 應能看到 UserB 的 Public 查詢"); } + [TestMethod] + public void ListSavedQueries_returns_403_when_user_lacks_required_role() + { + var controller = CreateControllerWithRoles("Viewer"); + var vmType = typeof(RestrictedSaleListVM).FullName; + + var result = controller.ListSavedQueries(vmType); + Assert.IsInstanceOfType(result, typeof(ForbidResult)); + } + + [TestMethod] + public void SaveQuery_returns_403_when_user_lacks_required_role() + { + var controller = CreateControllerWithRoles("Viewer"); + var req = new SaveQueryRequest + { + Name = "Restricted", + IsPublic = false, + Config = new AnalysisQueryRequest + { + ListVmType = typeof(RestrictedSaleListVM).FullName, + Dimensions = new List { "Region" }, + Measures = new List { new MeasureRequest { Field = "Amount", Func = AggregateFunc.Sum } } + } + }; + + var result = controller.SaveQuery(req); + Assert.IsInstanceOfType(result, typeof(ForbidResult)); + } + [TestMethod] public void GetSavedQuery_returns_config_for_authorized_user() { @@ -2540,6 +2570,30 @@ public void GetSavedQuery_returns_403_for_unauthorized_user() Assert.IsNotNull(result, "存取他人的私有查詢應回傳 403 Forbid"); } + [TestMethod] + public void GetSavedQuery_returns_403_when_public_query_vm_is_role_restricted() + { + var controller = CreateControllerWithRoles("Viewer"); + controller.Wtm.LoginUserInfo.ITCode = "userA"; + var id = Guid.NewGuid(); + var vmType = typeof(RestrictedSaleListVM).FullName; + controller.Wtm.DC.Set().Add( + new AnalysisSavedQuery + { + ID = id, + Name = "RestrictedPublic", + ListVmType = vmType, + OwnerCode = "userB", + IsPublic = true, + ConfigJson = "{\"listVmType\":\"" + vmType + "\"}" + } + ); + controller.Wtm.DC.SaveChanges(); + + var result = controller.GetSavedQuery(id); + Assert.IsInstanceOfType(result, typeof(ForbidResult)); + } + [TestMethod] public async Task DeleteSavedQuery_removes_record_if_owner() {