From 12f8c84c9df6ae459e168161e7d701b8a90713d8 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 0000000000..9f2a279e25 --- /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 e8671dd2de..460ca347f5 100644 --- a/src/Web/Grand.Web.Admin/Controllers/AffiliateController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/AffiliateController.cs @@ -73,7 +73,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 9ccc2cc185..a5988990ae 100644 --- a/src/Web/Grand.Web.Admin/Controllers/CampaignController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/CampaignController.cs @@ -133,7 +133,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 e075756491..a0963e6678 100644 --- a/src/Web/Grand.Web.Admin/Controllers/CourseController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/CourseController.cs @@ -144,7 +144,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) @@ -366,7 +366,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 f770079de0..550fe985da 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 a9ab6cbf2b..f82cf11791 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 918ddd9402..52977b06c7 100644 --- a/src/Web/Grand.Web.Admin/Controllers/KnowledgebaseController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/KnowledgebaseController.cs @@ -78,7 +78,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) @@ -181,7 +181,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 f289b60643..29863d3304 100644 --- a/src/Web/Grand.Web.Admin/Controllers/VendorController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/VendorController.cs @@ -115,7 +115,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 49e32ed2d3..8d17a48b66 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseBlogController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseBlogController.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(BlogPostModel model, bool continueEditing) @@ -251,7 +251,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 b778d4ae32..8d8736f8b3 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseBrandController.cs @@ -104,7 +104,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 50fda8d857..aca3237d9e 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseCategoryController.cs @@ -82,7 +82,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 41ab27700c..28badd4372 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseCollectionController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseCollectionController.cs @@ -105,7 +105,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 be7eefaf7d..f04f4c33d8 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseNewsController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseNewsController.cs @@ -80,7 +80,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 c091153957..3c8d5934ae 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BasePageController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BasePageController.cs @@ -88,7 +88,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 28c5c8b708..f7b7ce5054 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseProductController.cs @@ -96,7 +96,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 1013b42fce..4919c54bfd 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseShipmentController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseShipmentController.cs @@ -164,7 +164,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)