From 2b913517f9f920c3c8101404ce9bba2fb19f95f2 Mon Sep 17 00:00:00 2001 From: support Date: Mon, 2 Nov 2026 07:40:49 +0100 Subject: [PATCH] Require the Create action when saving a create form These forms require the Create action to open, but saving them checked Edit. A customer group allowed to create but not to edit was shown the form and then denied on save. Discounts, gift vouchers and customers already use Create on both sides. The new test covers every Admin and AdminShared action whose form requires Create, so a new action cannot drift from its form. --- .../CreateActionPermissionTests.cs | 40 +++++++++++++++++++ .../Controllers/AffiliateController.cs | 2 +- .../Controllers/CampaignController.cs | 2 +- .../Controllers/CourseController.cs | 4 +- .../Controllers/CustomerGroupController.cs | 2 +- .../Controllers/DocumentController.cs | 4 +- .../Controllers/KnowledgebaseController.cs | 4 +- .../Controllers/VendorController.cs | 2 +- .../Controllers/BaseBlogController.cs | 4 +- .../Controllers/BaseBrandController.cs | 2 +- .../Controllers/BaseCategoryController.cs | 2 +- .../Controllers/BaseCollectionController.cs | 2 +- .../Controllers/BaseNewsController.cs | 2 +- .../Controllers/BasePageController.cs | 2 +- .../Controllers/BaseProductController.cs | 2 +- .../Controllers/BaseShipmentController.cs | 2 +- 16 files changed, 59 insertions(+), 19 deletions(-) create mode 100644 src/Tests/Grand.Web.Admin.Tests/Controllers/CreateActionPermissionTests.cs diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/CreateActionPermissionTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/CreateActionPermissionTests.cs new file mode 100644 index 000000000..9f2a279e2 --- /dev/null +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/CreateActionPermissionTests.cs @@ -0,0 +1,40 @@ +using System.Reflection; +using Grand.Domain.Permissions; +using Grand.Web.Admin.Controllers; +using Grand.Web.AdminShared.Controllers; +using Grand.Web.Common.Security.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Grand.Web.Admin.Tests.Controllers; + +[TestClass] +public class CreateActionPermissionTests +{ + // Opening a create form and submitting it are one operation, so a form that requires + // PermissionActionName.Create has to be submitted under the same action. + [TestMethod] + public void CreateFormPost_RequiresTheSameActionAsItsForm() + { + var forms = new[] { typeof(BaseProductController).Assembly, typeof(VendorController).Assembly } + .SelectMany(assembly => assembly.GetTypes()) + .Where(type => typeof(Controller).IsAssignableFrom(type)) + .SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + .GroupBy(method => $"{method.DeclaringType!.Name}.{method.Name}") + .Where(action => action.Any(method => !IsPost(method) && RequiredAction(method) == PermissionActionName.Create)) + .ToList(); + + var mismatched = forms + .Where(action => action.Any(method => IsPost(method) && RequiredAction(method) != PermissionActionName.Create)) + .Select(action => action.Key) + .ToList(); + + Assert.IsTrue(forms.Any(action => action.Key == "BaseProductController.Create")); + Assert.AreEqual(0, mismatched.Count, string.Join(", ", mismatched)); + } + + private static bool IsPost(MethodInfo method) => method.IsDefined(typeof(HttpPostAttribute), false); + + private static string RequiredAction(MethodInfo method) => + method.GetCustomAttribute()?.PermissionAction; +} diff --git a/src/Web/Grand.Web.Admin/Controllers/AffiliateController.cs b/src/Web/Grand.Web.Admin/Controllers/AffiliateController.cs index 882c0ade5..211ac25f7 100644 --- a/src/Web/Grand.Web.Admin/Controllers/AffiliateController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/AffiliateController.cs @@ -72,7 +72,7 @@ public async Task Create() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(AffiliateModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.Admin/Controllers/CampaignController.cs b/src/Web/Grand.Web.Admin/Controllers/CampaignController.cs index 377bd9da3..a59367da1 100644 --- a/src/Web/Grand.Web.Admin/Controllers/CampaignController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/CampaignController.cs @@ -134,7 +134,7 @@ public async Task Create() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(CampaignModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.Admin/Controllers/CourseController.cs b/src/Web/Grand.Web.Admin/Controllers/CourseController.cs index f15c3edbe..2c8538ce2 100644 --- a/src/Web/Grand.Web.Admin/Controllers/CourseController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/CourseController.cs @@ -145,7 +145,7 @@ public async Task Create() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(CourseModel model, bool continueEditing) @@ -367,7 +367,7 @@ public async Task CreateLesson(string courseId) return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task CreateLesson(CourseLessonModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.Admin/Controllers/CustomerGroupController.cs b/src/Web/Grand.Web.Admin/Controllers/CustomerGroupController.cs index a4a3eefa8..b80f51ee6 100644 --- a/src/Web/Grand.Web.Admin/Controllers/CustomerGroupController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/CustomerGroupController.cs @@ -85,7 +85,7 @@ public IActionResult Create() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(CustomerGroupModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.Admin/Controllers/DocumentController.cs b/src/Web/Grand.Web.Admin/Controllers/DocumentController.cs index 53ab4bfd9..00a7aa2bb 100644 --- a/src/Web/Grand.Web.Admin/Controllers/DocumentController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/DocumentController.cs @@ -71,7 +71,7 @@ public async Task CreateDocument(SimpleDocumentModel simpleModel) return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task CreateDocument(DocumentModel model, bool continueEditing) @@ -170,7 +170,7 @@ public IActionResult CreateType() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task CreateType(DocumentTypeModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.Admin/Controllers/KnowledgebaseController.cs b/src/Web/Grand.Web.Admin/Controllers/KnowledgebaseController.cs index ec6a7e26a..336124aa1 100644 --- a/src/Web/Grand.Web.Admin/Controllers/KnowledgebaseController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/KnowledgebaseController.cs @@ -79,7 +79,7 @@ public async Task CreateCategory() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task CreateCategory(KnowledgebaseCategoryModel model, bool continueEditing) @@ -182,7 +182,7 @@ public async Task CreateArticle(string parentCategoryId) return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task CreateArticle(KnowledgebaseArticleModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.Admin/Controllers/VendorController.cs b/src/Web/Grand.Web.Admin/Controllers/VendorController.cs index f307da286..b0b0f8198 100644 --- a/src/Web/Grand.Web.Admin/Controllers/VendorController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/VendorController.cs @@ -116,7 +116,7 @@ public async Task Create() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(VendorModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseBlogController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseBlogController.cs index 3634c6c57..070ff3140 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseBlogController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseBlogController.cs @@ -91,7 +91,7 @@ public async Task Create() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(BlogPostModel model, bool continueEditing) @@ -252,7 +252,7 @@ public async Task CategoryCreate() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task CategoryCreate(BlogCategoryModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs index 7e581678d..6d2b6b928 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs @@ -106,7 +106,7 @@ public async Task Create([FromServices] CatalogSettings catalogSe return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(BrandModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs index df2203892..e69088955 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs @@ -84,7 +84,7 @@ public async Task Create() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(CategoryModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseCollectionController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseCollectionController.cs index c1fd03525..ce23f83a6 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseCollectionController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseCollectionController.cs @@ -107,7 +107,7 @@ public async Task Create([FromServices] CatalogSettings catalogSe return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(CollectionModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseNewsController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseNewsController.cs index 6acfef0a7..c9a586b84 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseNewsController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseNewsController.cs @@ -81,7 +81,7 @@ public async Task Create() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(NewsItemModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BasePageController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BasePageController.cs index 5ff9b9e1a..358d75e73 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BasePageController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BasePageController.cs @@ -90,7 +90,7 @@ public async Task Create() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(PageModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs index 8b5bbc758..8d42247c9 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs @@ -89,7 +89,7 @@ public async Task Create() return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task Create(ProductModel model, bool continueEditing) diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseShipmentController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseShipmentController.cs index af5bb6967..e0777223a 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseShipmentController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseShipmentController.cs @@ -180,7 +180,7 @@ public async Task AddShipment(string orderId) return View(model); } - [PermissionAuthorizeAction(PermissionActionName.Edit)] + [PermissionAuthorizeAction(PermissionActionName.Create)] [HttpPost] [ArgumentNameFilter(KeyName = "save-continue", Argument = "continueEditing")] public async Task AddShipment(AddShipmentModel model, bool continueEditing)