From 1ebda8560aea23b28cb9f3cb5aac7b29e5c25541 Mon Sep 17 00:00:00 2001 From: Gian Piero Bandieramonte Date: Wed, 11 Sep 2024 17:40:07 +0300 Subject: [PATCH 1/7] 2nd part of the customer authorized stores task. --- .../Extensions/OrderItemQueryExtensions.cs | 17 ++++ .../Orders/Extensions/OrderQueryExtensions.cs | 3 +- .../Extensions/ShipmentQueryExtensions.cs | 21 ++++- .../ICustomerStoreQueryExtensions.cs | 39 +++++++++ .../Extensions/CustomerQueryExtensions.cs | 33 ++++++++ .../Stores/Services/IStoreMappingService.cs | 9 ++- .../Stores/Services/StoreMappingService.cs | 30 +++++-- .../DashboardBestsellersViewComponent.cs | 4 +- .../DashboardIncompleteOrdersViewComponent.cs | 4 +- .../DashboardLatestOrdersViewComponent.cs | 4 +- ...shboardRegisteredCustomersViewComponent.cs | 10 +-- .../DashboardTopCustomersViewComponent.cs | 6 +- .../Admin/Controllers/CategoryController.cs | 3 + .../CheckoutAttributeController.cs | 3 + .../Admin/Controllers/CustomerController.cs | 80 ++++++++++++++++--- .../Controllers/CustomerRoleController.cs | 15 ++++ .../Controllers/ManufacturerController.cs | 3 + .../Admin/Controllers/OrderController.cs | 8 +- .../Controllers/ProductController.Grid.cs | 12 +-- .../Admin/Controllers/ShipmentController.cs | 3 +- .../Admin/Controllers/StoreController.cs | 56 +++++++------ .../Shared/EditorTemplates/Stores.cshtml | 10 +++ 22 files changed, 303 insertions(+), 70 deletions(-) create mode 100644 src/Smartstore.Core/Common/Extensions/ICustomerStoreQueryExtensions.cs diff --git a/src/Smartstore.Core/Checkout/Orders/Extensions/OrderItemQueryExtensions.cs b/src/Smartstore.Core/Checkout/Orders/Extensions/OrderItemQueryExtensions.cs index 70e14cff76..9ed1ec21bf 100644 --- a/src/Smartstore.Core/Checkout/Orders/Extensions/OrderItemQueryExtensions.cs +++ b/src/Smartstore.Core/Checkout/Orders/Extensions/OrderItemQueryExtensions.cs @@ -1,5 +1,6 @@ using Smartstore.Core.Checkout.Orders; using Smartstore.Core.Checkout.Orders.Reporting; +using Smartstore.Core.Checkout.Shipping; using Smartstore.Core.Data; namespace Smartstore @@ -189,5 +190,21 @@ public static IQueryable SelectAsBestsellersReportLine(th return selector; } + + /// + /// Selects order items that the currently authenticated customer is authorized to access. + /// + /// Order items query to filter from. + /// Ids of stores customer has access to + /// of . + public static IQueryable ApplyCustomerStoreFilter(this IQueryable query, int[] authorizedStoreIds) + { + Guard.NotNull(query); + if (!authorizedStoreIds.IsNullOrEmpty()) + { + query = query.Where(oi => authorizedStoreIds.Contains(oi.Order.StoreId)); + } + return query; + } } } diff --git a/src/Smartstore.Core/Checkout/Orders/Extensions/OrderQueryExtensions.cs b/src/Smartstore.Core/Checkout/Orders/Extensions/OrderQueryExtensions.cs index 237d834f67..cf1dfe1e66 100644 --- a/src/Smartstore.Core/Checkout/Orders/Extensions/OrderQueryExtensions.cs +++ b/src/Smartstore.Core/Checkout/Orders/Extensions/OrderQueryExtensions.cs @@ -350,8 +350,9 @@ public static Task GetOrdersTotalAsync(this IQueryable query) /// Selects customer authorized orders from query. /// /// Order query from which to select. + /// Ids of stores customer has access to /// of . - public static IQueryable ApplyCustomerFilter(this IQueryable query, int[] authorizedStoreIds) + public static IQueryable ApplyCustomerStoreFilter(this IQueryable query, int[] authorizedStoreIds) { Guard.NotNull(query); if (!authorizedStoreIds.IsNullOrEmpty()) diff --git a/src/Smartstore.Core/Checkout/Shipping/Extensions/ShipmentQueryExtensions.cs b/src/Smartstore.Core/Checkout/Shipping/Extensions/ShipmentQueryExtensions.cs index 546dc58f51..55ec19180d 100644 --- a/src/Smartstore.Core/Checkout/Shipping/Extensions/ShipmentQueryExtensions.cs +++ b/src/Smartstore.Core/Checkout/Shipping/Extensions/ShipmentQueryExtensions.cs @@ -1,4 +1,7 @@ -namespace Smartstore.Core.Checkout.Shipping +using Smartstore.Core.Checkout.Orders; +using Smartstore.Core.Checkout.Orders.Reporting; + +namespace Smartstore.Core.Checkout.Shipping { /// /// Shipment query extensions @@ -52,5 +55,21 @@ public static IOrderedQueryable ApplyShipmentFilter(this IQueryable x.Id) .ThenBy(x => x.CreatedOnUtc); } + + /// + /// Selects shipments that the currently authenticated customer is authorized to access. + /// + /// Shipment query to filter from. + /// Ids of stores customer has access to + /// of . + public static IQueryable ApplyCustomerStoreFilter(this IQueryable query, int[] authorizedStoreIds) + { + Guard.NotNull(query); + if (!authorizedStoreIds.IsNullOrEmpty()) + { + query = query.Where(s => authorizedStoreIds.Contains(s.Order.StoreId)); + } + return query; + } } } \ No newline at end of file diff --git a/src/Smartstore.Core/Common/Extensions/ICustomerStoreQueryExtensions.cs b/src/Smartstore.Core/Common/Extensions/ICustomerStoreQueryExtensions.cs new file mode 100644 index 0000000000..682b5efa73 --- /dev/null +++ b/src/Smartstore.Core/Common/Extensions/ICustomerStoreQueryExtensions.cs @@ -0,0 +1,39 @@ +using Smartstore.Core.Stores; + +namespace Smartstore +{ + public static partial class ICustomerStoreQueryExtensions + { + /// + /// Filters away items in a query belonging to stores to which a given authenticated customer is not authorized to access. + /// + /// Query of type from which to filter. + /// The stores the authenticated customer is authorized to access. + /// The mappings of all items of type T belonging to a limited number of stores. + /// of . + public static IQueryable ApplyCustomerStoreFilter( + this IQueryable query, + int[] customerAuthorizedStores, + StoreMappingCollection storeMappings) + where T : BaseEntity + { + if (customerAuthorizedStores.IsNullOrEmpty()) return query; + Guard.NotNull(query, nameof(query)); + + var groupedStoreMappings = storeMappings.GroupBy( + sm => sm.EntityId, + sm => sm.StoreId, + (key, g) => new { EntityId = key, StoreIdsList = g.ToList() }); + + foreach (var groupedMapping in groupedStoreMappings) + { + if (!customerAuthorizedStores.Any(casId => groupedMapping.StoreIdsList.Any(storeId => storeId == casId))) + { + query = query.Where(x => x.Id != groupedMapping.EntityId); + } + } + + return query; + } + } +} diff --git a/src/Smartstore.Core/Platform/Identity/Extensions/CustomerQueryExtensions.cs b/src/Smartstore.Core/Platform/Identity/Extensions/CustomerQueryExtensions.cs index 8f7fc42446..1ccda0652e 100644 --- a/src/Smartstore.Core/Platform/Identity/Extensions/CustomerQueryExtensions.cs +++ b/src/Smartstore.Core/Platform/Identity/Extensions/CustomerQueryExtensions.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore.Query; using Smartstore.Core.Catalog.Products; +using Smartstore.Core.Checkout.Orders; using Smartstore.Core.Data; namespace Smartstore.Core.Identity @@ -179,6 +180,23 @@ public static IQueryable ApplyRolesFilter(this IQueryable qu return query; } + + /// + /// Selects customers that the currently authenticated customer is authorized to access. + /// + /// Customers query to filter from. + /// Ids of stores customer has access to + /// of . + public static IQueryable ApplyCustomerStoreFilter(this IQueryable query, int[] authorizedStoreIds) + { + Guard.NotNull(query); + if (!authorizedStoreIds.IsNullOrEmpty()) + { + query = query.Where(c => authorizedStoreIds.Contains(c.Id)); + } + return query; + } + /// /// Selects customers who are currently online since and orders by descending. /// @@ -194,6 +212,21 @@ public static IOrderedQueryable ApplyOnlineCustomersFilter(this IQuery .ApplyLastActivityFilter(fromUtc, null); } + /// + /// Filters out super admins when the current customer is not a super admin - when isSuperAdmin = false. + /// + /// + public static IQueryable ApplySuperAdminFilter(this IQueryable query, bool isSuperAdmin) + { + Guard.NotNull(query); + + if (!isSuperAdmin) + { + return query.Where(customer => !customer.CustomerRoleMappings.Any(mapping => mapping.CustomerRole.SystemName == SystemCustomerRoleNames.SuperAdministrators)); + } + return query; + } + /// /// Selects customers who use given password . /// diff --git a/src/Smartstore.Core/Platform/Stores/Services/IStoreMappingService.cs b/src/Smartstore.Core/Platform/Stores/Services/IStoreMappingService.cs index d4a092b44d..d81130fc84 100644 --- a/src/Smartstore.Core/Platform/Stores/Services/IStoreMappingService.cs +++ b/src/Smartstore.Core/Platform/Stores/Services/IStoreMappingService.cs @@ -12,7 +12,8 @@ public interface IStoreMappingService /// Entity type /// The entity /// Array of selected store ids - Task ApplyStoreMappingsAsync(T entity, int[] selectedStoreIds) where T : BaseEntity, IStoreRestricted; + /// Returns false if customer user tries to add stores it has no access to, otherwise true. + Task ApplyStoreMappingsAsync(T entity, int[] selectedStoreIds) where T : BaseEntity, IStoreRestricted; /// /// Creates and adds a entity to the change tracker. @@ -45,6 +46,12 @@ public interface IStoreMappingService /// Store identifiers Task GetAuthorizedStoreIdsAsync(string entityName, int entityId); + /// + /// Finds store identifiers which currently authenticated customer has been granted access to + /// + /// Store identifiers + Task GetCustomerAuthorizedStoreIdsAsync(); + /// /// Prefetches a collection of store mappings for a range of entities in one go /// and caches them for the duration of the current request. diff --git a/src/Smartstore.Core/Platform/Stores/Services/StoreMappingService.cs b/src/Smartstore.Core/Platform/Stores/Services/StoreMappingService.cs index 05bece6a06..b41968f133 100644 --- a/src/Smartstore.Core/Platform/Stores/Services/StoreMappingService.cs +++ b/src/Smartstore.Core/Platform/Stores/Services/StoreMappingService.cs @@ -1,4 +1,5 @@ -using Smartstore.Caching; +using Autofac.Core; +using Smartstore.Caching; using Smartstore.Core.Data; using Smartstore.Data.Hooks; @@ -17,12 +18,14 @@ public partial class StoreMappingService : AsyncDbSaveHook, IStore private readonly IStoreContext _storeContext; private readonly ICacheManager _cache; private readonly IDictionary _prefetchedCollections; + private readonly IWorkContext _workContext; - public StoreMappingService(ICacheManager cache, IStoreContext storeContext, SmartDbContext db) + public StoreMappingService(ICacheManager cache, IStoreContext storeContext, SmartDbContext db, IWorkContext workContext) { _cache = cache; _storeContext = storeContext; _db = db; + _workContext = workContext; _prefetchedCollections = new Dictionary(StringComparer.OrdinalIgnoreCase); } @@ -51,14 +54,19 @@ public override async Task OnAfterSaveCompletedAsync(IEnumerable #endregion - public virtual async Task ApplyStoreMappingsAsync(T entity, int[] selectedStoreIds) + public virtual async Task ApplyStoreMappingsAsync(T entity, int[] selectedStoreIds) where T : BaseEntity, IStoreRestricted { - selectedStoreIds ??= Array.Empty(); - + var customerAuthorizedStores = await GetCustomerAuthorizedStoreIdsAsync(); + selectedStoreIds ??= (!_workContext.CurrentCustomer.IsSuperAdmin() ? customerAuthorizedStores : []) ; + if (customerAuthorizedStores.Length > 0 && selectedStoreIds.Any(ssId => !customerAuthorizedStores.Any(cas => ssId == cas))) + { + //Trying to select a store not in the list of authorized stores of the customer making this change + return false; + } + List lookup = null; var allStores = _storeContext.GetAllStores(); - entity.LimitedToStores = (selectedStoreIds.Length != 1 || selectedStoreIds[0] != 0) && selectedStoreIds.Any(); foreach (var store in allStores) @@ -81,6 +89,7 @@ public virtual async Task ApplyStoreMappingsAsync(T entity, int[] selectedSto } } } + return true; } public virtual void AddStoreMapping(T entity, int storeId) where T : BaseEntity, IStoreRestricted @@ -126,19 +135,24 @@ public virtual async Task GetAuthorizedStoreIdsAsync(string entityName, i if (entityId <= 0) { - return Array.Empty(); + return []; } var cacheSegment = await GetCacheSegmentAsync(entityName, entityId); if (!cacheSegment.TryGetValue(entityId, out var storeIds)) { - return Array.Empty(); + return []; } return storeIds; } + public virtual async Task GetCustomerAuthorizedStoreIdsAsync() + { + return await GetAuthorizedStoreIdsAsync("Customer", _workContext.CurrentCustomer.Id); + } + public virtual async Task PrefetchStoreMappingsAsync(string entityName, int[] entityIds, bool isRange = false, bool isSorted = false, bool tracked = false) { var collection = await GetStoreMappingCollectionAsync(entityName, entityIds, isRange, isSorted, tracked); diff --git a/src/Smartstore.Web/Areas/Admin/Components/DashboardBestsellersViewComponent.cs b/src/Smartstore.Web/Areas/Admin/Components/DashboardBestsellersViewComponent.cs index 498cd585c2..decb7f58e1 100644 --- a/src/Smartstore.Web/Areas/Admin/Components/DashboardBestsellersViewComponent.cs +++ b/src/Smartstore.Web/Areas/Admin/Components/DashboardBestsellersViewComponent.cs @@ -21,14 +21,14 @@ public async Task InvokeAsync() } var customer = Services.WorkContext.CurrentCustomer; - var authorizedStoreIds = await Services.StoreMappingService.GetAuthorizedStoreIdsAsync("Customer", customer.Id); + var authorizedStoreIds = await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(); const int pageSize = 7; // INFO: join tables to ignore soft-deleted products and orders. var orderItemQuery = from oi in _db.OrderItems.AsNoTracking() - join o in _db.Orders.ApplyCustomerFilter(authorizedStoreIds).AsNoTracking() on oi.OrderId equals o.Id + join o in _db.Orders.ApplyCustomerStoreFilter(authorizedStoreIds).AsNoTracking() on oi.OrderId equals o.Id join p in _db.Products.AsNoTracking() on oi.ProductId equals p.Id where !p.IsSystemProduct select oi; diff --git a/src/Smartstore.Web/Areas/Admin/Components/DashboardIncompleteOrdersViewComponent.cs b/src/Smartstore.Web/Areas/Admin/Components/DashboardIncompleteOrdersViewComponent.cs index 895dc05902..5fe1327878 100644 --- a/src/Smartstore.Web/Areas/Admin/Components/DashboardIncompleteOrdersViewComponent.cs +++ b/src/Smartstore.Web/Areas/Admin/Components/DashboardIncompleteOrdersViewComponent.cs @@ -25,7 +25,7 @@ public override async Task InvokeAsync() var primaryCurrency = Services.CurrencyService.PrimaryCurrency; var customer = Services.WorkContext.CurrentCustomer; - var authorizedStoreIds = await Services.StoreMappingService.GetAuthorizedStoreIdsAsync("Customer", customer.Id); + var authorizedStoreIds = await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(); var model = new List { @@ -43,7 +43,7 @@ public override async Task InvokeAsync() .AsNoTracking() .ApplyAuditDateFilter(CreatedFrom, null) .ApplyIncompleteOrdersFilter() - .ApplyCustomerFilter(authorizedStoreIds) + .ApplyCustomerStoreFilter(authorizedStoreIds) .Select(x => new OrderDataPoint { CreatedOn = x.CreatedOnUtc, diff --git a/src/Smartstore.Web/Areas/Admin/Components/DashboardLatestOrdersViewComponent.cs b/src/Smartstore.Web/Areas/Admin/Components/DashboardLatestOrdersViewComponent.cs index c57c9a95a7..fae811b397 100644 --- a/src/Smartstore.Web/Areas/Admin/Components/DashboardLatestOrdersViewComponent.cs +++ b/src/Smartstore.Web/Areas/Admin/Components/DashboardLatestOrdersViewComponent.cs @@ -23,11 +23,11 @@ public async Task InvokeAsync() } var customer = Services.WorkContext.CurrentCustomer; - var authorizedStoreIds = await Services.StoreMappingService.GetAuthorizedStoreIdsAsync("Customer", customer.Id); + var authorizedStoreIds = await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(); var model = new DashboardLatestOrdersModel(); var latestOrders = await _db.Orders - .ApplyCustomerFilter(authorizedStoreIds) + .ApplyCustomerStoreFilter(authorizedStoreIds) .AsNoTracking() .AsSplitQuery() .Include(x => x.Customer) diff --git a/src/Smartstore.Web/Areas/Admin/Components/DashboardRegisteredCustomersViewComponent.cs b/src/Smartstore.Web/Areas/Admin/Components/DashboardRegisteredCustomersViewComponent.cs index b5cabccf55..1d1a061473 100644 --- a/src/Smartstore.Web/Areas/Admin/Components/DashboardRegisteredCustomersViewComponent.cs +++ b/src/Smartstore.Web/Areas/Admin/Components/DashboardRegisteredCustomersViewComponent.cs @@ -8,10 +8,7 @@ public class DashboardRegisteredCustomersViewComponent : DashboardViewComponentB { private readonly SmartDbContext _db; - public DashboardRegisteredCustomersViewComponent(SmartDbContext db) - { - _db = db; - } + public DashboardRegisteredCustomersViewComponent(SmartDbContext db) => _db = db; public override async Task InvokeAsync() { @@ -25,9 +22,12 @@ public override async Task InvokeAsync() .FirstOrDefaultAsync(x => x.SystemName == SystemCustomerRoleNames.Registered); var customerDates = _db.Customers + .ApplyCustomerStoreFilter( + await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(Customer), [.. _db.Customers.Select(x => x.Id)])) .AsNoTracking() .ApplyRegistrationFilter(CreatedFrom, Now) - .ApplyRolesFilter(new[] { registeredRole.Id }) + .ApplyRolesFilter([registeredRole.Id]) .Select(x => x.CreatedOnUtc) .ToList(); diff --git a/src/Smartstore.Web/Areas/Admin/Components/DashboardTopCustomersViewComponent.cs b/src/Smartstore.Web/Areas/Admin/Components/DashboardTopCustomersViewComponent.cs index 204818f103..279db865bd 100644 --- a/src/Smartstore.Web/Areas/Admin/Components/DashboardTopCustomersViewComponent.cs +++ b/src/Smartstore.Web/Areas/Admin/Components/DashboardTopCustomersViewComponent.cs @@ -22,10 +22,8 @@ public async Task InvokeAsync() return Empty(); } - var customer = Services.WorkContext.CurrentCustomer; - var authorizedStoreIds = await Services.StoreMappingService.GetAuthorizedStoreIdsAsync("Customer", customer.Id); - - var orderQuery = _db.Orders.Where(x => !x.Customer.Deleted).ApplyCustomerFilter(authorizedStoreIds); + var authorizedStoreIds = await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(); + var orderQuery = _db.Orders.Where(x => !x.Customer.Deleted).ApplyCustomerStoreFilter(authorizedStoreIds); var reportByQuantity = await orderQuery .SelectAsTopCustomerReportLine(ReportSorting.ByQuantityDesc) diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/CategoryController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/CategoryController.cs index eab6bbfc11..be57084654 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/CategoryController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/CategoryController.cs @@ -179,6 +179,9 @@ public async Task CategoryList(GridCommand command, CategoryListM var categories = await query .ApplyStandardFilter(true, null, model.SearchStoreId) + .ApplyCustomerStoreFilter( + await _storeMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await _storeMappingService.GetStoreMappingCollectionAsync(nameof(Category), [.. query.Select(x => x.Id)])) .ApplyGridCommand(command, false) .ToPagedList(command) .LoadAsync(); diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/CheckoutAttributeController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/CheckoutAttributeController.cs index b539da22c5..74e53937ea 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/CheckoutAttributeController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/CheckoutAttributeController.cs @@ -59,6 +59,9 @@ public async Task CheckoutAttributeList(GridCommand command) var model = new GridModel(); var checkoutAttributes = await _db.CheckoutAttributes + .ApplyCustomerStoreFilter( + await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(CheckoutAttribute), [.. _db.CheckoutAttributes.Select(x => x.Id)])) .AsNoTracking() .ApplyStandardFilter(true) .ApplyGridCommand(command) diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs index 7185a677c3..467053fb98 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs @@ -48,7 +48,6 @@ public class CustomerController : AdminController private readonly Lazy _shippingService; private readonly Lazy _paymentService; private readonly ShoppingCartSettings _shoppingCartSettings; - private readonly IStoreMappingService _storeMappingService; public CustomerController( SmartDbContext db, @@ -69,8 +68,7 @@ public CustomerController( Lazy shoppingCartService, Lazy shippingService, Lazy paymentService, - ShoppingCartSettings shoppingCartSettings, - IStoreMappingService storeMappingService) + ShoppingCartSettings shoppingCartSettings) { _db = db; _customerService = customerService; @@ -91,7 +89,6 @@ public CustomerController( _shippingService = shippingService; _paymentService = paymentService; _shoppingCartSettings = shoppingCartSettings; - _storeMappingService = storeMappingService; } #region Utilities @@ -141,7 +138,7 @@ private async Task PrepareCustomerModel(CustomerModel model, Customer customer) model.AllowManagingCustomerRoles = await Services.Permissions.AuthorizeAsync(Permissions.Customer.EditRole); model.CustomerNumberEnabled = _customerSettings.CustomerNumberMethod != CustomerNumberMethod.Disabled; model.UsernamesEnabled = _customerSettings.CustomerLoginType != CustomerLoginType.Email; - model.SelectedStoreIds = await _storeMappingService.GetAuthorizedStoreIdsAsync(customer); + model.SelectedStoreIds = await Services.StoreMappingService.GetAuthorizedStoreIdsAsync(customer); if (customer != null) { @@ -426,7 +423,7 @@ public async Task List() CompanyEnabled = _customerSettings.CompanyEnabled, PhoneEnabled = _customerSettings.PhoneEnabled, ZipPostalCodeEnabled = _customerSettings.ZipPostalCodeEnabled, - SearchCustomerRoleIds = new int[] { registeredRole.Id } + SearchCustomerRoleIds = [registeredRole.Id] }; return View(listModel); @@ -442,7 +439,11 @@ public async Task CustomerList(GridCommand command, CustomerListM .Include(x => x.ShippingAddress) .IncludeCustomerRoles() .ApplyIdentFilter(model.SearchEmail, model.SearchUsername, model.SearchCustomerNumber) - .ApplyBirthDateFilter(model.SearchYearOfBirth.ToInt(), model.SearchMonthOfBirth.ToInt(), model.SearchDayOfBirth.ToInt()); + .ApplyBirthDateFilter(model.SearchYearOfBirth.ToInt(), model.SearchMonthOfBirth.ToInt(), model.SearchDayOfBirth.ToInt()) + .ApplyCustomerStoreFilter( + await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(Customer), [.. _db.Customers.Select(x => x.Id)])) + .ApplySuperAdminFilter(Services.WorkContext.CurrentCustomer.IsSuperAdmin()); if (model.SearchCustomerRoleIds != null) { @@ -535,6 +536,13 @@ public async Task Create(CustomerModel model, bool continueEditin LastActivityDateUtc = DateTime.UtcNow }; + // Validate super admin + if (!ValidateSuperAdmin(model.SelectedCustomerRoleIds)) + { + NotifyAccessDenied(); + return RedirectToAction(nameof(Create), new { customer.Id }); + } + // Validate customer roles. var allCustomerRoleIds = await _db.CustomerRoles.Select(x => x.Id).ToListAsync(); var (newCustomerRoles, customerRolesError) = await ValidateCustomerRoles(model.SelectedCustomerRoleIds, allCustomerRoleIds); @@ -578,7 +586,10 @@ public async Task Create(CustomerModel model, bool continueEditin }); }); - await _storeMappingService.ApplyStoreMappingsAsync(customer, model.SelectedStoreIds); + if (!await Services.StoreMappingService.ApplyStoreMappingsAsync(customer, model.SelectedStoreIds)) + { + NotifyError("Unauthorized stores removed."); + } await _db.SaveChangesAsync(); await Services.EventPublisher.PublishAsync(new ModelBoundEvent(model, customer, form)); @@ -624,6 +635,31 @@ public async Task Edit(int id) return View(model); } + /// + /// Controls that: + /// - only super admins can add new super admins + /// - if there is no existing super admin, then any admin can give itself super admin priviledges + /// - if there is already a super admin, then no admins can give itself super admin priviledges + /// + /// + /// true if validation passed, otherwise false + private bool ValidateSuperAdmin(int[] selectedCustomerRoleIds) + { + // Only if there is currently no super admin, allow an admin customer to set itself as super admin. + var superAdminRole = _db.CustomerRoles.FirstOrDefault(x => x.SystemName == SystemCustomerRoleNames.SuperAdministrators); + if (!Services.WorkContext.CurrentCustomer.IsSuperAdmin() && selectedCustomerRoleIds.Any(x => x == superAdminRole?.Id)) + { + var superAdminExists = _db.Customers.Any( + customer => customer.CustomerRoleMappings.Any( + mapping => mapping.CustomerRole.SystemName == SystemCustomerRoleNames.SuperAdministrators)); + if (superAdminExists) + { + return false; + } + } + return true; + } + [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] [FormValueRequired("save", "save-continue")] [Permission(Permissions.Customer.Update)] @@ -639,12 +675,25 @@ public async Task Edit(CustomerModel model, bool continueEditing, return NotFound(); } + if (customer.IsSuperAdmin() && !Services.WorkContext.CurrentCustomer.IsSuperAdmin()) + { + NotifyAccessDenied(); + return RedirectToAction(nameof(Edit), new { customer.Id }); + } + if (customer.IsAdmin() && !Services.WorkContext.CurrentCustomer.IsAdmin()) { NotifyAccessDenied(); return RedirectToAction(nameof(Edit), new { customer.Id }); } + // Validate super admin + if (!ValidateSuperAdmin(model.SelectedCustomerRoleIds)) + { + NotifyAccessDenied(); + return RedirectToAction(nameof(Edit), new { customer.Id }); + } + // Validate customer roles. var allowManagingCustomerRoles = Services.Permissions.Authorize(Permissions.Customer.EditRole); @@ -765,7 +814,10 @@ public async Task Edit(CustomerModel model, bool continueEditing, await scope.CommitAsync(); } - await _storeMappingService.ApplyStoreMappingsAsync(customer, model.SelectedStoreIds); + if (!await Services.StoreMappingService.ApplyStoreMappingsAsync(customer, model.SelectedStoreIds)) + { + NotifyError("Unauthorized stores removed."); + } await _db.SaveChangesAsync(); await Services.EventPublisher.PublishAsync(new ModelBoundEvent(model, customer, form)); @@ -1013,6 +1065,10 @@ public async Task OnlineCustomersList(GridCommand command) .IncludeCustomerRoles() .Where(x => !x.IsSystemAccount) .ApplyOnlineCustomersFilter(_customerSettings.OnlineCustomerMinutes) + .ApplyCustomerStoreFilter( + await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(Customer), [.. _db.Customers.Select(x => x.Id)])) + .ApplySuperAdminFilter(Services.WorkContext.CurrentCustomer.IsSuperAdmin()) .ApplyGridCommand(command) .ToPagedList(command) .LoadAsync(); @@ -1318,7 +1374,10 @@ async Task GetRegisteredCustomersReport(int days) var startDate = Services.DateTimeHelper.ConvertToUserTime(DateTime.Now).AddDays(-days); return await _db.Customers - .ApplyRolesFilter(new[] { registeredRoleId }) + .ApplyCustomerStoreFilter( + await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(Customer), [.. _db.Customers.Select(x => x.Id)])) + .ApplyRolesFilter([registeredRoleId]) .ApplyRegistrationFilter(startDate, null) .CountAsync(); } @@ -1342,6 +1401,7 @@ public async Task ReportTopCustomersList(GridCommand command, Top var shippingStatusIds = model.ShippingStatusId > 0 ? new[] { model.ShippingStatusId } : null; var orderQuery = _db.Orders + .ApplyCustomerStoreFilter(await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync()) .Where(x => !x.Customer.Deleted) .ApplyStatusFilter(orderStatusIds, paymentStatusIds, shippingStatusIds) .ApplyAuditDateFilter(startDate, endDate); diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/CustomerRoleController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/CustomerRoleController.cs index d352d5610f..13aa2f9071 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/CustomerRoleController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/CustomerRoleController.cs @@ -33,6 +33,7 @@ public class CustomerRoleController : AdminController private readonly Lazy _taskStore; private readonly Lazy _taskScheduler; private readonly CustomerSettings _customerSettings; + private readonly IWorkContext _workContext; public CustomerRoleController( SmartDbContext db, @@ -54,6 +55,7 @@ public CustomerRoleController( /// /// (AJAX) Gets a list of all available customer roles. + /// Exclude super admin role from list if there is already a super admin and currently logged in customer is not super admin. /// /// Text for optional entry. If not null an entry with the specified label text and the Id 0 will be added to the list. /// Ids of selected entities. @@ -68,6 +70,17 @@ public async Task AllCustomerRoles(string label, string selectedI query = query.Where(x => !x.IsSystemRole); } + if (!Services.WorkContext.CurrentCustomer.IsSuperAdmin()) + { + var superAdminExists = _db.Customers.Any( + customer => customer.CustomerRoleMappings.Any( + mapping => mapping.CustomerRole.SystemName == SystemCustomerRoleNames.SuperAdministrators)); + if (superAdminExists) + { + query = query.Where(x => x.SystemName != SystemCustomerRoleNames.SuperAdministrators); + } + } + query = query.ApplyStandardFilter(true); var rolesPager = new FastPager(query, 1000); @@ -116,8 +129,10 @@ public IActionResult List() [Permission(Permissions.Customer.Role.Read)] public async Task RoleList(GridCommand command) { + var isSuperAdmin = Services.WorkContext.CurrentCustomer.IsSuperAdmin(); var customerRoles = await _roleManager.Roles .AsNoTracking() + .Where(x => isSuperAdmin || x.SystemName != SystemCustomerRoleNames.SuperAdministrators) .OrderBy(x => x.Name) .ApplyGridCommand(command) .ToPagedList(command) diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs index 8d864ee252..69be65bdd8 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs @@ -133,6 +133,9 @@ public async Task ManufacturerList(GridCommand command, Manufactu var manufacturers = await query .ApplyStandardFilter(true, null, model.SearchStoreId) + .ApplyCustomerStoreFilter( + await _storeMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await _storeMappingService.GetStoreMappingCollectionAsync(nameof(Manufacturer), [.. query.Select(x => x.Id)])) .ApplyGridCommand(command, false) .ToPagedList(command) .LoadAsync(); diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs index 71318b5948..c49f8fdcb9 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs @@ -166,8 +166,6 @@ public async Task OrderList(GridCommand command, OrderListModel m var withPaymentMethodString = T("Admin.Order.WithPaymentMethod").Value; var fromStoreString = T("Admin.Order.FromStore").Value; var paymentMethodSystemnames = model.PaymentMethods.SplitSafe(',').ToArray(); - var customer = Services.WorkContext.CurrentCustomer; - var authorizedStoreIds = await Services.StoreMappingService.GetAuthorizedStoreIdsAsync("Customer", customer.Id); DateTime? startDateUtc = model.StartDate == null ? null @@ -186,7 +184,7 @@ public async Task OrderList(GridCommand command, OrderListModel m .ApplyAuditDateFilter(startDateUtc, endDateUtc) .ApplyStatusFilter(model.OrderStatusIds, model.PaymentStatusIds, model.ShippingStatusIds) .ApplyPaymentFilter(paymentMethodSystemnames) - .ApplyCustomerFilter(authorizedStoreIds); + .ApplyCustomerStoreFilter(await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync()); if (productId > 0) { @@ -1719,7 +1717,9 @@ public async Task BestsellersReportList(GridCommand command, Best var orderItemQuery = _db.OrderItems .AsNoTracking() .ApplyOrderFilter(0, startDate, endDate, orderStatusId, paymentStatusId, shippingStatusId, countryId) - .ApplyProductFilter(null, true); + .ApplyProductFilter(null, true) + .Include(x => x.Order) + .ApplyCustomerStoreFilter([.. (await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync())]); var reportLines = await orderItemQuery .SelectAsBestsellersReportLine(sorting) diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.Grid.cs b/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.Grid.cs index 4781d0b6bc..0f9982d430 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.Grid.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.Grid.cs @@ -28,15 +28,17 @@ public async Task ProductList(GridCommand command, ProductListMod var query = _catalogSearchService.Value .PrepareQuery(searchQuery) .ApplyGridCommand(command, false); - - products = await query.ToPagedList(command).LoadAsync(); + + products = await query + .ApplyCustomerStoreFilter( + await _storeMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await _storeMappingService.GetStoreMappingCollectionAsync(nameof(Product), [.. query.Select(x => x.Id)])) + .ToPagedList(command).LoadAsync(); } - var rows = await products.MapAsync(Services.MediaService); - return Json(new GridModel { - Rows = rows, + Rows = await products.MapAsync(Services.MediaService), Total = products.TotalCount }); } diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/ShipmentController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/ShipmentController.cs index 10243c30a6..ad6aaac868 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/ShipmentController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/ShipmentController.cs @@ -102,9 +102,10 @@ public async Task ShipmentList(GridCommand command, ShipmentListM .Where(x => x.Order != null) .ApplyTimeFilter(startDate, endDate) .ApplyGridCommand(command, false) + .ApplyCustomerStoreFilter([.. (await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync())]) .ToPagedList(command) .LoadAsync(); - + var rows = await shipments.SelectAwait(async x => { var m = new ShipmentModel(); diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/StoreController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/StoreController.cs index 08331a716b..4dcee6bd5e 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/StoreController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/StoreController.cs @@ -3,6 +3,10 @@ using Smartstore.Admin.Models.Store; using Smartstore.Admin.Models.Stores; using Smartstore.ComponentModel; +using Smartstore.Core.Catalog.Attributes; +using Smartstore.Core.Catalog.Brands; +using Smartstore.Core.Catalog.Categories; +using Smartstore.Core.Catalog.Products; using Smartstore.Core.Catalog.Search; using Smartstore.Core.Checkout.Cart; using Smartstore.Core.Content.Media; @@ -23,7 +27,7 @@ public class StoreController : AdminController private readonly ShoppingCartSettings _shoppingCartSettings; public StoreController( - SmartDbContext db, + SmartDbContext db, ICatalogSearchService catalogSearchService, ShoppingCartSettings shoppingCartSettings) { @@ -76,9 +80,11 @@ public IActionResult List() public async Task StoreList(GridCommand command) { var stores = Services.StoreContext.GetAllStores(); + var customerAuthorizedStores = await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(); var mapper = MapperFactory.GetMapper(); var rows = await stores + .Where(store => customerAuthorizedStores.Length != 0 ? customerAuthorizedStores.Any(cas => store.Id == cas) : true) .AsQueryable() .ApplyGridCommand(command) .SelectAwait(async x => @@ -210,36 +216,38 @@ public async Task Delete(int id) public async Task StoreDashboardReportAsync() { var primaryCurrency = Services.CurrencyService.PrimaryCurrency; - - var customer = Services.WorkContext.CurrentCustomer; - var authorizedStoreIds = await Services.StoreMappingService.GetAuthorizedStoreIdsAsync("Customer", customer.Id); - - var ordersQuery = _db.Orders.ApplyCustomerFilter(authorizedStoreIds).AsNoTracking(); - var registeredRole = await _db.CustomerRoles - .AsNoTracking() - .FirstOrDefaultAsync(x => x.SystemName == SystemCustomerRoleNames.Registered); - - var registeredCustomersQuery = _db.Customers - .AsNoTracking() - .ApplyRolesFilter([registeredRole.Id]); - + var authorizedStoreIds = await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(); + + var customerStoreMappings = await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(Customer), [.. _db.Customers.Select(x => x.Id)]); + var productStoreMappings = await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(Product), [.. _db.Products.Select(x => x.Id)]); + var categoryStoreMappings = await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(Category), [.. _db.Categories.Select(x => x.Id)]); + var manufacturerStoreMappings = await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(Manufacturer), [.. _db.MediaFiles.Select(x => x.Id)]); + var attributesCountStoreMappings = await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(ProductAttribute), [.. _db.ProductAttributes.Select(x => x.Id)]); + var attributeCombinationsCountStoreMappings = await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(ProductVariantAttributeCombination), [.. _db.MediaFiles.Select(x => x.Id)]); + var shoppingCartItemStoreMappings = await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(ShoppingCartItem), [.. _db.ShoppingCartItems.Select(x => x.Id)]); + var mediaFileStoreMappings = await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(MediaFile), [.. _db.MediaFiles.Select(x => x.Id)]); + var filteredCustomers = _db.Customers.ApplyCustomerStoreFilter(authorizedStoreIds, customerStoreMappings); + + var registeredRole = await _db.CustomerRoles.AsNoTracking().FirstOrDefaultAsync(x => x.SystemName == SystemCustomerRoleNames.Registered); + var ordersQuery = _db.Orders.ApplyCustomerStoreFilter(authorizedStoreIds).AsNoTracking(); var sumAllOrders = await ordersQuery.SumAsync(x => (decimal?)x.OrderTotal) ?? 0; - var sumOpenCarts = await _db.ShoppingCartItems.GetOpenCartTypeSubTotalAsync(ShoppingCartType.ShoppingCart, _shoppingCartSettings.AllowActivatableCartItems ? true : null); - var sumWishlists = await _db.ShoppingCartItems.GetOpenCartTypeSubTotalAsync(ShoppingCartType.Wishlist); - var totalMediaSize = await _db.MediaFiles.SumAsync(x => (long)x.Size); + var shoppingCartItems = _db.ShoppingCartItems.ApplyCustomerStoreFilter(authorizedStoreIds, shoppingCartItemStoreMappings); + var sumOpenCarts = await shoppingCartItems.GetOpenCartTypeSubTotalAsync(ShoppingCartType.ShoppingCart, _shoppingCartSettings.AllowActivatableCartItems ? true : null); + var sumWishlists = await shoppingCartItems.GetOpenCartTypeSubTotalAsync(ShoppingCartType.Wishlist); + var totalMediaSize = await _db.MediaFiles.ApplyCustomerStoreFilter(authorizedStoreIds, mediaFileStoreMappings).SumAsync(x => (long)x.Size); var model = new StoreDashboardReportModel { - ProductsCount = (await _catalogSearchService.PrepareQuery(new CatalogSearchQuery()).CountAsync()).ToString("N0"), - CategoriesCount = (await _db.Categories.CountAsync()).ToString("N0"), - ManufacturersCount = (await _db.Manufacturers.CountAsync()).ToString("N0"), - AttributesCount = (await _db.ProductAttributes.CountAsync()).ToString("N0"), - AttributeCombinationsCount = (await _db.ProductVariantAttributeCombinations.CountAsync(x => x.IsActive)).ToString("N0"), + ProductsCount = (await _catalogSearchService.PrepareQuery(new CatalogSearchQuery()).ApplyCustomerStoreFilter(authorizedStoreIds, productStoreMappings).CountAsync()).ToString("N0"), + CategoriesCount = (await _db.Categories.ApplyCustomerStoreFilter(authorizedStoreIds, categoryStoreMappings).CountAsync()).ToString("N0"), + ManufacturersCount = (await _db.Manufacturers.ApplyCustomerStoreFilter(authorizedStoreIds, manufacturerStoreMappings).CountAsync()).ToString("N0"), + AttributesCount = (await _db.ProductAttributes.ApplyCustomerStoreFilter(authorizedStoreIds, attributesCountStoreMappings).CountAsync()).ToString("N0"), + AttributeCombinationsCount = (await _db.ProductVariantAttributeCombinations.ApplyCustomerStoreFilter(authorizedStoreIds, attributeCombinationsCountStoreMappings).CountAsync(x => x.IsActive)).ToString("N0"), MediaCount = (await Services.MediaService.CountFilesAsync(new MediaSearchQuery { Deleted = false })).ToString("N0"), MediaSize = Prettifier.HumanizeBytes(totalMediaSize), - CustomersCount = (await registeredCustomersQuery.CountAsync()).ToString("N0"), + CustomersCount = (await filteredCustomers.AsNoTracking().ApplyRolesFilter([registeredRole.Id]).CountAsync()).ToString("N0"), + OnlineCustomersCount = (await filteredCustomers.ApplyOnlineCustomersFilter(15).CountAsync()).ToString("N0"), OrdersCount = (await ordersQuery.CountAsync()).ToString("N0"), - OnlineCustomersCount = (await _db.Customers.ApplyOnlineCustomersFilter(15).CountAsync()).ToString("N0"), Sales = Services.CurrencyService.CreateMoney(sumAllOrders, primaryCurrency).ToString(), CartsValue = Services.CurrencyService.CreateMoney(sumOpenCarts, primaryCurrency).ToString(), WishlistsValue = Services.CurrencyService.CreateMoney(sumWishlists, primaryCurrency).ToString() diff --git a/src/Smartstore.Web/Areas/Admin/Views/Shared/EditorTemplates/Stores.cshtml b/src/Smartstore.Web/Areas/Admin/Views/Shared/EditorTemplates/Stores.cshtml index 5e7dcac435..f5f4920317 100644 --- a/src/Smartstore.Web/Areas/Admin/Views/Shared/EditorTemplates/Stores.cshtml +++ b/src/Smartstore.Web/Areas/Admin/Views/Shared/EditorTemplates/Stores.cshtml @@ -1,8 +1,11 @@ @using System.Globalization @using Smartstore.Utilities @using Smartstore.Core.Stores +@using Smartstore.Core @inject IStoreContext StoreContext +@inject IWorkContext workContext +@inject IStoreMappingService StoreMappingService @functions { @@ -32,6 +35,13 @@ var items = StoreContext.GetAllStores().ToSelectListItems(SelectedIds); var attributes = new AttributeDictionary().Merge(ConvertUtility.ObjectToDictionary(ViewData["htmlAttributes"] ?? new object())); + var authorizedStoreIds = await StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(); + if (authorizedStoreIds.Count() > 0) + { + //Admin is limited to at least one store. + items = items.Where(x => authorizedStoreIds.Contains(int.Parse(x.Value))).ToList(); + } + if (multiple && !attributes.ContainsKey("data-placeholder")) { attributes["data-placeholder"] = T("Admin.Common.StoresAll").Value; From 4aea9cff57a6280b2569b5ecb621b1331c899ef1 Mon Sep 17 00:00:00 2001 From: Gian Piero Bandieramonte Date: Fri, 13 Sep 2024 13:35:05 +0300 Subject: [PATCH 2/7] Task completed. --- .../Themes/Flex/wwwroot/_variables.scss | 7 +- .../Product/Partials/Product.Info.cshtml | 14 --- .../Views/Product/Product.cshtml | 33 ++++-- .../Views/Shared/Partials/MediaGallery.cshtml | 110 +++++++++++------- 4 files changed, 101 insertions(+), 63 deletions(-) diff --git a/src/Smartstore.Web/Themes/Flex/wwwroot/_variables.scss b/src/Smartstore.Web/Themes/Flex/wwwroot/_variables.scss index 38a1b5d1ba..8501937bba 100644 --- a/src/Smartstore.Web/Themes/Flex/wwwroot/_variables.scss +++ b/src/Smartstore.Web/Themes/Flex/wwwroot/_variables.scss @@ -66,7 +66,12 @@ $grays: ( $spacer: 1.25rem; $spacers: ( - 6: ($spacer * 4.5) + 6: ($spacer * 4.5), + 7: ($spacer * 6), + 8: ($spacer * 7.5), + 9: ($spacer * 9), + 10: ($spacer * 11), + 11: ($spacer * 13), ); $theme-colors: ( diff --git a/src/Smartstore.Web/Views/Product/Partials/Product.Info.cshtml b/src/Smartstore.Web/Views/Product/Partials/Product.Info.cshtml index f9f0dd15a7..a2aa722671 100644 --- a/src/Smartstore.Web/Views/Product/Partials/Product.Info.cshtml +++ b/src/Smartstore.Web/Views/Product/Partials/Product.Info.cshtml @@ -26,20 +26,6 @@ - -
-

- @if (Model.IsAssociatedProduct && Model.VisibleIndividually) - { - @Html.Raw(Model.Name) - } - else - { - @Html.Raw(Model.Name) - } -

-
- @if (Model.ShortDescription.Value.HasValue()) { diff --git a/src/Smartstore.Web/Views/Product/Product.cshtml b/src/Smartstore.Web/Views/Product/Product.cshtml index 60e3d2bdce..967665ca7a 100644 --- a/src/Smartstore.Web/Views/Product/Product.cshtml +++ b/src/Smartstore.Web/Views/Product/Product.cshtml @@ -12,21 +12,38 @@ { Assets.AppendCanonicalUrlParts(Model.CanonicalUrl); } + bool hasVideo = Model.MediaGalleryModel.Files.Any(f => f.MediaType == MediaType.Video); }
- +
-
+
@if (Model.Files.Count > 0) { for (int i = 0; i < Model.Files.Count; i++) { var file = Model.Files[i]; + if (file.MediaType == MediaType.Video) continue; var attrName = (i == Model.GalleryStartIndex ? "src" : "data-lazy"); var attrValue = file.GetUrl(Model.ImageSize); var srcAttributes = new AttributeDictionary { [attrName] = attrValue }; @@ -75,25 +73,12 @@ data-thumb-image="@file.GetUrl(Model.ThumbSize)" data-medium-image="@file.GetUrl(Model.ImageSize)" data-picture-id="@file.Id"> - @if (file.MediaType == MediaType.Image) - { - @file.Alt - } - else - { -
- - @file.Alt - -
- } + @file.Alt
} @@ -108,6 +93,51 @@ }
+ + +
+
+ @for (int i = 0; i < Model.Files.Count; i++) + { + var file = Model.Files[i]; + var isVideo = file.MediaType == MediaType.Video; + if (!isVideo) continue; + var attrName = (i == Model.GalleryStartIndex ? "src" : "data-lazy"); + var attrValue = file.GetUrl(Model.ImageSize); + var srcAttributes = new AttributeDictionary { [attrName] = attrValue }; + +
+ +
+ } +
+
+
+ + +
+
+ @for (int i = 0; i < Model.Files.Count; i++) + { + var file = Model.Files[i]; + var isVideo = file.MediaType == MediaType.Video; + if (!isVideo) continue; + var attrName = (i == Model.GalleryStartIndex ? "src" : "data-lazy"); + var attrValue = file.GetUrl(Model.ImageSize); + var srcAttributes = new AttributeDictionary { [attrName] = attrValue }; + +
+ + @* *@ +
+ } +
From 522931f4e4a64f022b64568166dc77992a9ab257 Mon Sep 17 00:00:00 2001 From: Gian Piero Bandieramonte Date: Tue, 17 Sep 2024 18:28:11 -0500 Subject: [PATCH 3/7] Added backend validation to prevent users from editing models they are not authorized to access by means of manipulating URLs. --- .../ProductReviewQueryExtensions.cs | 38 ++++++ .../Security/Services/IPermissionService.cs | 17 +++ .../Security/Services/PermissionService.cs | 115 +++++++++++++++--- .../Admin/Controllers/CategoryController.cs | 19 +-- .../CheckoutAttributeController.cs | 7 ++ .../Admin/Controllers/CustomerController.cs | 50 ++------ .../Controllers/ManufacturerController.cs | 7 ++ .../Admin/Controllers/OrderController.cs | 6 + .../Admin/Controllers/ProductController.cs | 8 +- .../Controllers/ProductReviewController.cs | 9 ++ .../Admin/Controllers/ShipmentController.cs | 6 + 11 files changed, 213 insertions(+), 69 deletions(-) create mode 100644 src/Smartstore.Core/Catalog/Products/Extensions/ProductReviewQueryExtensions.cs diff --git a/src/Smartstore.Core/Catalog/Products/Extensions/ProductReviewQueryExtensions.cs b/src/Smartstore.Core/Catalog/Products/Extensions/ProductReviewQueryExtensions.cs new file mode 100644 index 0000000000..f076f29f63 --- /dev/null +++ b/src/Smartstore.Core/Catalog/Products/Extensions/ProductReviewQueryExtensions.cs @@ -0,0 +1,38 @@ +using Smartstore.Core.Stores; + +namespace Smartstore.Core.Catalog.Products +{ + public static partial class ProductReviewQueryExtensions + { + /// + /// Filters away items in a query belonging to stores to which a given authenticated customer is not authorized to access. + /// + /// Query of type from which to filter. + /// The stores the authenticated customer is authorized to access. + /// The mappings of all items of type T belonging to a limited number of stores. + /// of . + public static IQueryable ApplyReviewStoreFilter( + this IQueryable query, + int[] customerAuthorizedStores, + StoreMappingCollection storeMappings) + { + if (customerAuthorizedStores.IsNullOrEmpty()) return query; + Guard.NotNull(query, nameof(query)); + + var groupedStoreMappings = storeMappings.GroupBy( + sm => sm.EntityId, + sm => sm.StoreId, + (key, g) => new { EntityId = key, StoreIdsList = g.ToList() }); + + foreach (var groupedMapping in groupedStoreMappings) + { + if (!customerAuthorizedStores.Any(casId => groupedMapping.StoreIdsList.Any(storeId => storeId == casId))) + { + query = query.Where(x => x.ProductId != groupedMapping.EntityId); + } + } + + return query; + } + } +} diff --git a/src/Smartstore.Core/Platform/Security/Services/IPermissionService.cs b/src/Smartstore.Core/Platform/Security/Services/IPermissionService.cs index 74becf2a0d..a0f0787fa3 100644 --- a/src/Smartstore.Core/Platform/Security/Services/IPermissionService.cs +++ b/src/Smartstore.Core/Platform/Security/Services/IPermissionService.cs @@ -73,5 +73,22 @@ public interface IPermissionService /// Providers whose permissions are to be installed. /// Whether to remove permissions no longer supported by the providers. Task InstallPermissionsAsync(IPermissionProvider[] permissionProviders, bool removeUnusedPermissions = false); + + /// + /// Controls that: + /// - only super admins can add new super admins + /// - if there is no existing super admin, then any admin can give itself super admin priviledges + /// - if there is already a super admin, then no admins can give itself super admin priviledges + /// + /// Role Ids that are currently selected from the customer being edited + /// true if validation passed, otherwise false + bool ValidateSuperAdmin(int[] selectedCustomerRoleIds); + + /// + /// Forbids customers from entering into unauthorized customers' edit pages by manipulating the url. + /// + /// The entity intended to be edited by currently authenticated customer + /// true if authenticated customer is authorized, false otherwise + Task CanAccessEntity(T entity) where T : BaseEntity; } } \ No newline at end of file diff --git a/src/Smartstore.Core/Platform/Security/Services/PermissionService.cs b/src/Smartstore.Core/Platform/Security/Services/PermissionService.cs index 48e3cf0af6..4e69b49d9b 100644 --- a/src/Smartstore.Core/Platform/Security/Services/PermissionService.cs +++ b/src/Smartstore.Core/Platform/Security/Services/PermissionService.cs @@ -1,16 +1,25 @@ using System.Text; using Smartstore.Caching; using Smartstore.Collections; +using Smartstore.Core.Catalog.Products; +using Smartstore.Core.Checkout.Orders; +using Smartstore.Core.Checkout.Shipping; using Smartstore.Core.Data; using Smartstore.Core.Identity; using Smartstore.Core.Localization; +using Smartstore.Core.Stores; using Smartstore.Data; using Smartstore.Data.Hooks; using EState = Smartstore.Data.EntityState; namespace Smartstore.Core.Security { - public partial class PermissionService : AsyncDbSaveHook, IPermissionService + public partial class PermissionService( + SmartDbContext db, + Lazy workContext, + ILocalizationService localizationService, + ICacheManager cache, + IStoreMappingService storeMappingService) : AsyncDbSaveHook, IPermissionService { // {0} = roleId private readonly static CompositeFormat PERMISSION_TREE_KEY = CompositeFormat.Parse("permission:tree-{0}"); @@ -90,25 +99,14 @@ public partial class PermissionService : AsyncDbSaveHook, IPermiss { "rule", "Common.Rules" }, }; - private readonly SmartDbContext _db; - private readonly Lazy _workContext; - private readonly ILocalizationService _localizationService; - private readonly ICacheManager _cache; + private readonly SmartDbContext _db = db; + private readonly Lazy _workContext = workContext; + private readonly ILocalizationService _localizationService = localizationService; + private readonly ICacheManager _cache = cache; + private readonly IStoreMappingService _storeMappingService = storeMappingService; private string _hookErrorMessage; - public PermissionService( - SmartDbContext db, - Lazy workContext, - ILocalizationService localizationService, - ICacheManager cache) - { - _db = db; - _workContext = workContext; - _localizationService = localizationService; - _cache = cache; - } - #region Hook protected override Task OnUpdatedAsync(CustomerRole entity, IHookedEntity entry, CancellationToken cancelToken) @@ -328,7 +326,7 @@ public virtual async Task> GetAllSystemNamesAsync() public virtual async Task GetDisplayNameAsync(string permissionSystemName) { var tokens = permissionSystemName.EmptyNull().ToLower().Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries); - if (tokens.Any()) + if (tokens.Length != 0) { var resourcesLookup = await GetDisplayNameLookup(_workContext.Value.WorkingLanguage.Id); @@ -352,7 +350,6 @@ public virtual async Task InstallPermissionsAsync(IPermissionProvider[] permissi { return; } - var allPermissionNames = await _db.PermissionRecords .AsQueryable() .Select(x => x.SystemName) @@ -365,7 +362,7 @@ public virtual async Task InstallPermissionsAsync(IPermissionProvider[] permissi var log = existing.Any(); var clearCache = false; - if (existing.Any()) + if (existing.Count != 0) { var permissionsMigrated = existing.Contains(Permissions.System.AccessShop) && !existing.Contains("PublicStoreAllowNavigation"); if (!permissionsMigrated) @@ -692,5 +689,83 @@ private async Task> GetDisplayNameLookup(int language } #endregion + + #region Store restricted utilities + + public bool ValidateSuperAdmin(int[] selectedCustomerRoleIds) + { + var superAdminRole = _db.CustomerRoles.FirstOrDefault(x => x.SystemName == SystemCustomerRoleNames.SuperAdministrators); + + // Only if there is currently no super admin, allow an admin customer to set itself as super admin. + if (!_workContext.Value.CurrentCustomer.IsSuperAdmin() && selectedCustomerRoleIds.Any(x => x == superAdminRole?.Id)) + { + var superAdminExists = _db.Customers.Any( + customer => customer.CustomerRoleMappings.Any( + mapping => mapping.CustomerRole.SystemName == SystemCustomerRoleNames.SuperAdministrators)); + if (superAdminExists) + { + return false; + } + } + return true; + } + + public async Task CanAccessEntity(T entity) where T : BaseEntity + { + var customerAuthorizedStores = await _storeMappingService.GetCustomerAuthorizedStoreIdsAsync(); + if (customerAuthorizedStores.Length == 0) + { + return true; + } + + switch (typeof(T).Name) + { + case "Order": + var order = entity as Order; + if (!customerAuthorizedStores.Any(casId => order.StoreId == casId)) + { + return false; + } + break; + case "Shipment": + var shipment = entity as Shipment; + if (!customerAuthorizedStores.Any(casId => shipment.Order.StoreId == casId)) + { + return false; + } + break; + case "ProductReview": + var productReview = entity as ProductReview; + var prStoreMappings = await _storeMappingService.GetStoreMappingCollectionAsync(nameof(Product), [productReview.ProductId]); + if (prStoreMappings.Count != 0 && !customerAuthorizedStores.Any(casId => prStoreMappings.Any(storeMapping => storeMapping.StoreId == casId))) + { + return false; + } + break; + default: + var storeMappings = await _storeMappingService.GetStoreMappingCollectionAsync(typeof(T).Name, [entity.Id]); + if (storeMappings.Count != 0 && !customerAuthorizedStores.Any(casId => storeMappings.Any(storeMapping => storeMapping.StoreId == casId))) + { + return false; + } + break; + } + + try + { + var customer = (Customer)Convert.ChangeType(entity, typeof(Customer)); + if ((customer.IsAdmin() && !_workContext.Value.CurrentCustomer.IsAdmin()) || (customer.IsSuperAdmin() && !_workContext.Value.CurrentCustomer.IsSuperAdmin())) + { + return false; + } + return true; + } + catch (Exception) + { + return true; + } + } + + #endregion } } diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/CategoryController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/CategoryController.cs index be57084654..6274b5228a 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/CategoryController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/CategoryController.cs @@ -8,6 +8,7 @@ using Smartstore.Core.Catalog.Discounts; using Smartstore.Core.Catalog.Products; using Smartstore.Core.Catalog.Rules; +using Smartstore.Core.Checkout.Shipping; using Smartstore.Core.Localization; using Smartstore.Core.Logging; using Smartstore.Core.Rules; @@ -30,7 +31,6 @@ public class CategoryController : AdminController private readonly ILocalizedEntityService _localizedEntityService; private readonly IDiscountService _discountService; private readonly IRuleService _ruleService; - private readonly IStoreMappingService _storeMappingService; private readonly IAclService _aclService; private readonly Lazy _taskStore; private readonly Lazy _taskScheduler; @@ -57,7 +57,6 @@ public CategoryController( _localizedEntityService = localizedEntityService; _discountService = discountService; _ruleService = ruleService; - _storeMappingService = storeMappingService; _aclService = aclService; _taskStore = taskStore; _taskScheduler = taskScheduler; @@ -180,8 +179,8 @@ public async Task CategoryList(GridCommand command, CategoryListM var categories = await query .ApplyStandardFilter(true, null, model.SearchStoreId) .ApplyCustomerStoreFilter( - await _storeMappingService.GetCustomerAuthorizedStoreIdsAsync(), - await _storeMappingService.GetStoreMappingCollectionAsync(nameof(Category), [.. query.Select(x => x.Id)])) + await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(Category), [.. query.Select(x => x.Id)])) .ApplyGridCommand(command, false) .ToPagedList(command) .LoadAsync(); @@ -329,7 +328,7 @@ public async Task Create(CategoryModel model, bool continueEditin await _discountService.ApplyDiscountsAsync(category, model?.SelectedDiscountIds, DiscountType.AssignedToCategories); await _ruleService.ApplyRuleSetMappingsAsync(category, model.SelectedRuleSetIds); - await _storeMappingService.ApplyStoreMappingsAsync(category, model.SelectedStoreIds); + await Services.StoreMappingService.ApplyStoreMappingsAsync(category, model.SelectedStoreIds); await _aclService.ApplyAclMappingsAsync(category, model.SelectedCustomerRoleIds); await _db.SaveChangesAsync(); @@ -363,6 +362,12 @@ public async Task Edit(int id) return NotFound(); } + if (!await Services.Permissions.CanAccessEntity(category)) + { + NotifyAccessDenied(); + return RedirectToAction(nameof(List)); + } + var mapper = MapperFactory.GetMapper(); var model = await mapper.MapAsync(category); @@ -411,7 +416,7 @@ public async Task Edit(CategoryModel model, bool continueEditing, await ApplyLocales(model, category); await _discountService.ApplyDiscountsAsync(category, model?.SelectedDiscountIds, DiscountType.AssignedToCategories); await _ruleService.ApplyRuleSetMappingsAsync(category, model.SelectedRuleSetIds); - await _storeMappingService.ApplyStoreMappingsAsync(category, model.SelectedStoreIds); + await Services.StoreMappingService.ApplyStoreMappingsAsync(category, model.SelectedStoreIds); await _aclService.ApplyAclMappingsAsync(category, model.SelectedCustomerRoleIds); _db.Categories.Update(category); @@ -618,7 +623,7 @@ private async Task PrepareCategoryModel(CategoryModel model, Category category) model.UpdatedOn = Services.DateTimeHelper.ConvertToUserTime(category.UpdatedOnUtc, DateTimeKind.Utc); model.CreatedOn = Services.DateTimeHelper.ConvertToUserTime(category.CreatedOnUtc, DateTimeKind.Utc); model.SelectedDiscountIds = category.AppliedDiscounts.Select(x => x.Id).ToArray(); - model.SelectedStoreIds = await _storeMappingService.GetAuthorizedStoreIdsAsync(category); + model.SelectedStoreIds = await Services.StoreMappingService.GetAuthorizedStoreIdsAsync(category); model.SelectedCustomerRoleIds = await _aclService.GetAuthorizedCustomerRoleIdsAsync(category); model.SelectedRuleSetIds = category.RuleSets.Select(x => x.Id).ToArray(); model.CategoryUrl = await GetEntityPublicUrlAsync(category); diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/CheckoutAttributeController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/CheckoutAttributeController.cs index 74e53937ea..b27866af3d 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/CheckoutAttributeController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/CheckoutAttributeController.cs @@ -2,6 +2,7 @@ using Smartstore.Admin.Models.Orders; using Smartstore.ComponentModel; using Smartstore.Core.Checkout.Attributes; +using Smartstore.Core.Checkout.Shipping; using Smartstore.Core.Common.Configuration; using Smartstore.Core.Common.Services; using Smartstore.Core.Localization; @@ -170,6 +171,12 @@ public async Task Edit(int id) return NotFound(); } + if (!await Services.Permissions.CanAccessEntity(checkoutAttribute)) + { + NotifyAccessDenied(); + return RedirectToAction(nameof(List)); + } + var model = await MapperFactory.MapAsync(checkoutAttribute); AddLocales(model.Locales, (locale, languageId) => diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs index 57f87b9573..313cc98ca2 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs @@ -537,7 +537,7 @@ public async Task Create(CustomerModel model, bool continueEditin }; // Validate super admin - if (!ValidateSuperAdmin(model.SelectedCustomerRoleIds)) + if (!Services.Permissions.ValidateSuperAdmin(model.SelectedCustomerRoleIds)) { NotifyAccessDenied(); return RedirectToAction(nameof(Create), new { customer.Id }); @@ -629,37 +629,17 @@ public async Task Edit(int id) return NotFound(); } + if (!await Services.Permissions.CanAccessEntity(customer)) + { + NotifyAccessDenied(); + return RedirectToAction(nameof(List)); + } + var model = new CustomerModel(); await PrepareCustomerModel(model, customer); - return View(model); } - /// - /// Controls that: - /// - only super admins can add new super admins - /// - if there is no existing super admin, then any admin can give itself super admin priviledges - /// - if there is already a super admin, then no admins can give itself super admin priviledges - /// - /// - /// true if validation passed, otherwise false - private bool ValidateSuperAdmin(int[] selectedCustomerRoleIds) - { - // Only if there is currently no super admin, allow an admin customer to set itself as super admin. - var superAdminRole = _db.CustomerRoles.FirstOrDefault(x => x.SystemName == SystemCustomerRoleNames.SuperAdministrators); - if (!Services.WorkContext.CurrentCustomer.IsSuperAdmin() && selectedCustomerRoleIds.Any(x => x == superAdminRole?.Id)) - { - var superAdminExists = _db.Customers.Any( - customer => customer.CustomerRoleMappings.Any( - mapping => mapping.CustomerRole.SystemName == SystemCustomerRoleNames.SuperAdministrators)); - if (superAdminExists) - { - return false; - } - } - return true; - } - [HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")] [FormValueRequired("save", "save-continue")] [Permission(Permissions.Customer.Update)] @@ -674,21 +654,9 @@ public async Task Edit(CustomerModel model, bool continueEditing, { return NotFound(); } - - if (customer.IsSuperAdmin() && !Services.WorkContext.CurrentCustomer.IsSuperAdmin()) - { - NotifyAccessDenied(); - return RedirectToAction(nameof(Edit), new { customer.Id }); - } - - if (customer.IsAdmin() && !Services.WorkContext.CurrentCustomer.IsAdmin()) - { - NotifyAccessDenied(); - return RedirectToAction(nameof(Edit), new { customer.Id }); - } - + // Validate super admin - if (!ValidateSuperAdmin(model.SelectedCustomerRoleIds)) + if (!Services.Permissions.ValidateSuperAdmin(model.SelectedCustomerRoleIds)) { NotifyAccessDenied(); return RedirectToAction(nameof(Edit), new { customer.Id }); diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs index 69be65bdd8..213ddf995d 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs @@ -7,6 +7,7 @@ using Smartstore.Core.Catalog.Brands; using Smartstore.Core.Catalog.Discounts; using Smartstore.Core.Catalog.Products; +using Smartstore.Core.Checkout.Shipping; using Smartstore.Core.Localization; using Smartstore.Core.Logging; using Smartstore.Core.Rules.Filters; @@ -243,6 +244,12 @@ public async Task Edit(int id) return NotFound(); } + if (!await Services.Permissions.CanAccessEntity(manufacturer)) + { + NotifyAccessDenied(); + return RedirectToAction(nameof(List)); + } + var mapper = MapperFactory.GetMapper(); var model = await mapper.MapAsync(manufacturer); diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs index 9889d07498..6011100e90 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs @@ -873,6 +873,12 @@ public async Task Edit(int id) return NotFound(); } + if(! await Services.Permissions.CanAccessEntity(order)) + { + NotifyAccessDenied(); + return RedirectToAction(nameof(List)); + } + var model = new OrderModel(); await PrepareOrderModel(model, order); diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.cs index 08dd5db9a2..eb46dcd46e 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/ProductController.cs @@ -257,13 +257,19 @@ public async Task Edit(int id) .Include(x => x.ProductTags) .Include(x => x.AppliedDiscounts) .FindByIdAsync(id); - + if (product == null) { NotifyWarning(T("Products.NotFound", id)); return RedirectToAction(nameof(List)); } + if (!await Services.Permissions.CanAccessEntity(product)) + { + NotifyAccessDenied(); + return RedirectToAction(nameof(List)); + } + if (product.Deleted) { NotifyWarning(T("Products.Deleted", id)); diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/ProductReviewController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/ProductReviewController.cs index 5e0b3c7185..142c6b44c6 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/ProductReviewController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/ProductReviewController.cs @@ -65,6 +65,9 @@ public async Task ProductReviewList(GridCommand command, ProductR : dtHelper.ConvertToUtcTime(model.CreatedOnTo.Value, dtHelper.CurrentTimeZone).AddDays(1); var query = _db.ProductReviews + .ApplyReviewStoreFilter( + await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(Product), [.. _db.ProductReviews.Select(x => x.ProductId)])) .AsSplitQuery() .Include(x => x.Product) .Include(x => x.Customer).ThenInclude(x => x.CustomerRoleMappings).ThenInclude(x => x.CustomerRole) @@ -180,6 +183,12 @@ public async Task Edit(int id) return NotFound(); } + if (!await Services.Permissions.CanAccessEntity(productReview)) + { + NotifyAccessDenied(); + return RedirectToAction(nameof(List)); + } + var model = new ProductReviewModel(); PrepareProductReviewModel(model, productReview, false, false); diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/ShipmentController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/ShipmentController.cs index 0992053660..c1d5ec176b 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/ShipmentController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/ShipmentController.cs @@ -236,6 +236,12 @@ public async Task Edit(int id) return NotFound(); } + if(!await Services.Permissions.CanAccessEntity(shipment)) + { + NotifyAccessDenied(); + return RedirectToAction(nameof(List)); + } + var model = new ShipmentModel(); await PrepareShipmentModel(model, shipment, true); PrepareViewBag(); From 155a26f4a55dfbee922c380f08b6820d798904ca Mon Sep 17 00:00:00 2001 From: Gian Piero Bandieramonte Date: Tue, 17 Sep 2024 18:37:49 -0500 Subject: [PATCH 4/7] Fixed tests. --- .../Platform/Security/PermissionServiceTests.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/Smartstore.Core.Tests/Platform/Security/PermissionServiceTests.cs b/test/Smartstore.Core.Tests/Platform/Security/PermissionServiceTests.cs index 037644d44d..e57b0d8ccc 100644 --- a/test/Smartstore.Core.Tests/Platform/Security/PermissionServiceTests.cs +++ b/test/Smartstore.Core.Tests/Platform/Security/PermissionServiceTests.cs @@ -7,6 +7,7 @@ using Smartstore.Core.Identity; using Smartstore.Core.Localization; using Smartstore.Core.Security; +using Smartstore.Core.Stores; namespace Smartstore.Core.Tests.Platform.Security { @@ -17,6 +18,7 @@ public class PermissionServiceTests : ServiceTestBase private ILocalizationService _localizationService; private IWorkContext _workContext; private ICacheManager _cacheManager; + private IStoreMappingService _storeMappingService; private readonly CustomerRole _rAdmin = new() { Id = 10, Active = true, SystemName = "Administrators", Name = "Administrators" }; private readonly CustomerRole _rModerator = new() { Id = 20, Active = true, SystemName = "Moderators", Name = "Moderators" }; @@ -43,7 +45,8 @@ public virtual void Setup() DbContext, new Lazy(() => _workContext), _localizationService, - _cacheManager); + _cacheManager, + _storeMappingService); } [Test] From daf8e8589afda18fa537dcbe8145ebb9cf359950 Mon Sep 17 00:00:00 2001 From: Gian Piero Bandieramonte Date: Sat, 5 Oct 2024 14:19:18 -0500 Subject: [PATCH 5/7] Enhanced how super admins deal with multi store entities. Now they can choose to filter by any store, or to add/edit any entity into any store regardless of which stores they currently have access to. --- .../Platform/Security/Services/PermissionService.cs | 2 +- .../Platform/Stores/Services/StoreMappingService.cs | 4 ++-- .../Areas/Admin/Controllers/ManufacturerController.cs | 3 +-- .../Areas/Admin/Views/Shared/EditorTemplates/Stores.cshtml | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/Smartstore.Core/Platform/Security/Services/PermissionService.cs b/src/Smartstore.Core/Platform/Security/Services/PermissionService.cs index 4e69b49d9b..01738d32f2 100644 --- a/src/Smartstore.Core/Platform/Security/Services/PermissionService.cs +++ b/src/Smartstore.Core/Platform/Security/Services/PermissionService.cs @@ -713,7 +713,7 @@ public bool ValidateSuperAdmin(int[] selectedCustomerRoleIds) public async Task CanAccessEntity(T entity) where T : BaseEntity { var customerAuthorizedStores = await _storeMappingService.GetCustomerAuthorizedStoreIdsAsync(); - if (customerAuthorizedStores.Length == 0) + if (_workContext.Value.CurrentCustomer.IsSuperAdmin() || customerAuthorizedStores.Length == 0) { return true; } diff --git a/src/Smartstore.Core/Platform/Stores/Services/StoreMappingService.cs b/src/Smartstore.Core/Platform/Stores/Services/StoreMappingService.cs index b41968f133..89bc537d4e 100644 --- a/src/Smartstore.Core/Platform/Stores/Services/StoreMappingService.cs +++ b/src/Smartstore.Core/Platform/Stores/Services/StoreMappingService.cs @@ -59,7 +59,7 @@ public virtual async Task ApplyStoreMappingsAsync(T entity, int[] selec { var customerAuthorizedStores = await GetCustomerAuthorizedStoreIdsAsync(); selectedStoreIds ??= (!_workContext.CurrentCustomer.IsSuperAdmin() ? customerAuthorizedStores : []) ; - if (customerAuthorizedStores.Length > 0 && selectedStoreIds.Any(ssId => !customerAuthorizedStores.Any(cas => ssId == cas))) + if (!_workContext.CurrentCustomer.IsSuperAdmin() && customerAuthorizedStores.Length > 0 && selectedStoreIds.Any(ssId => !customerAuthorizedStores.Any(cas => ssId == cas))) { //Trying to select a store not in the list of authorized stores of the customer making this change return false; @@ -150,7 +150,7 @@ public virtual async Task GetAuthorizedStoreIdsAsync(string entityName, i public virtual async Task GetCustomerAuthorizedStoreIdsAsync() { - return await GetAuthorizedStoreIdsAsync("Customer", _workContext.CurrentCustomer.Id); + return _workContext.CurrentCustomer.IsSuperAdmin() ? [] : await GetAuthorizedStoreIdsAsync("Customer", _workContext.CurrentCustomer.Id); } public virtual async Task PrefetchStoreMappingsAsync(string entityName, int[] entityIds, bool isRange = false, bool isSorted = false, bool tracked = false) diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs index 213ddf995d..c4466fb500 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs @@ -7,7 +7,6 @@ using Smartstore.Core.Catalog.Brands; using Smartstore.Core.Catalog.Discounts; using Smartstore.Core.Catalog.Products; -using Smartstore.Core.Checkout.Shipping; using Smartstore.Core.Localization; using Smartstore.Core.Logging; using Smartstore.Core.Rules.Filters; @@ -131,7 +130,7 @@ public async Task ManufacturerList(GridCommand command, Manufactu { query = query.ApplySearchFilterFor(x => x.Name, model.SearchManufacturerName); } - + var manufacturers = await query .ApplyStandardFilter(true, null, model.SearchStoreId) .ApplyCustomerStoreFilter( diff --git a/src/Smartstore.Web/Areas/Admin/Views/Shared/EditorTemplates/Stores.cshtml b/src/Smartstore.Web/Areas/Admin/Views/Shared/EditorTemplates/Stores.cshtml index f5f4920317..4198e75052 100644 --- a/src/Smartstore.Web/Areas/Admin/Views/Shared/EditorTemplates/Stores.cshtml +++ b/src/Smartstore.Web/Areas/Admin/Views/Shared/EditorTemplates/Stores.cshtml @@ -36,7 +36,7 @@ var attributes = new AttributeDictionary().Merge(ConvertUtility.ObjectToDictionary(ViewData["htmlAttributes"] ?? new object())); var authorizedStoreIds = await StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(); - if (authorizedStoreIds.Count() > 0) + if (!CommonServices.WorkContext.CurrentCustomer.IsSuperAdmin() && authorizedStoreIds.Count() > 0) { //Admin is limited to at least one store. items = items.Where(x => authorizedStoreIds.Contains(int.Parse(x.Value))).ToList(); From bf1364e5e39efba2c33c8a8646f818e994932a72 Mon Sep 17 00:00:00 2001 From: Gian Piero Bandieramonte Date: Mon, 7 Oct 2024 12:26:17 -0500 Subject: [PATCH 6/7] Fixed customer creation process. --- .../Areas/Admin/Controllers/CustomerController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs b/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs index 4caf7737b4..aa744f0c6e 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/CustomerController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.Rendering; using Newtonsoft.Json; +using NUglify.Helpers; using Smartstore.Admin.Models.Cart; using Smartstore.Admin.Models.Customers; using Smartstore.Admin.Models.Scheduling; @@ -223,7 +224,7 @@ await _db.LoadCollectionAsync(customer, x => x.Addresses, false, q => q ViewBag.AvailableTimeZones = dtHelper.GetSystemTimeZones() .ToSelectListItems(model.TimeZoneId.NullEmpty() ?? dtHelper.DefaultStoreTimeZone.Id); - ViewBag.IsAdmin = customer.IsAdmin() || customer.IsSuperAdmin(); + ViewBag.IsAdmin = customer != null && (customer.IsAdmin() || customer.IsSuperAdmin()); // Countries and state provinces. if (_customerSettings.CountryEnabled && model.CountryId > 0) From 1de0d4a1dfc18d2ff120eff9a36728145ec451ed Mon Sep 17 00:00:00 2001 From: Gian Piero Bandieramonte Date: Mon, 14 Oct 2024 10:16:17 -0500 Subject: [PATCH 7/7] Reverted changes not belonging to this issue. --- .../Themes/Flex/wwwroot/_variables.scss | 7 +- .../Product/Partials/Product.Info.cshtml | 14 +++ .../Views/Product/Product.cshtml | 21 +--- .../Views/Shared/Partials/MediaGallery.cshtml | 110 +++++++----------- 4 files changed, 57 insertions(+), 95 deletions(-) diff --git a/src/Smartstore.Web/Themes/Flex/wwwroot/_variables.scss b/src/Smartstore.Web/Themes/Flex/wwwroot/_variables.scss index 8501937bba..38a1b5d1ba 100644 --- a/src/Smartstore.Web/Themes/Flex/wwwroot/_variables.scss +++ b/src/Smartstore.Web/Themes/Flex/wwwroot/_variables.scss @@ -66,12 +66,7 @@ $grays: ( $spacer: 1.25rem; $spacers: ( - 6: ($spacer * 4.5), - 7: ($spacer * 6), - 8: ($spacer * 7.5), - 9: ($spacer * 9), - 10: ($spacer * 11), - 11: ($spacer * 13), + 6: ($spacer * 4.5) ); $theme-colors: ( diff --git a/src/Smartstore.Web/Views/Product/Partials/Product.Info.cshtml b/src/Smartstore.Web/Views/Product/Partials/Product.Info.cshtml index a2aa722671..f9f0dd15a7 100644 --- a/src/Smartstore.Web/Views/Product/Partials/Product.Info.cshtml +++ b/src/Smartstore.Web/Views/Product/Partials/Product.Info.cshtml @@ -26,6 +26,20 @@ + +
+

+ @if (Model.IsAssociatedProduct && Model.VisibleIndividually) + { + @Html.Raw(Model.Name) + } + else + { + @Html.Raw(Model.Name) + } +

+
+ @if (Model.ShortDescription.Value.HasValue()) { diff --git a/src/Smartstore.Web/Views/Product/Product.cshtml b/src/Smartstore.Web/Views/Product/Product.cshtml index 967665ca7a..320f501fd3 100644 --- a/src/Smartstore.Web/Views/Product/Product.cshtml +++ b/src/Smartstore.Web/Views/Product/Product.cshtml @@ -12,7 +12,6 @@ { Assets.AppendCanonicalUrlParts(Model.CanonicalUrl); } - bool hasVideo = Model.MediaGalleryModel.Files.Any(f => f.MediaType == MediaType.Video); } @@ -26,24 +25,8 @@
- - -
-

- @if (Model.IsAssociatedProduct && Model.VisibleIndividually) - { - @Html.Raw(Model.Name) - } - else - { - @Html.Raw(Model.Name) - } - -

-
- -
+
@@ -56,7 +39,7 @@
-
@@ -56,14 +59,13 @@ -
+
@if (Model.Files.Count > 0) { for (int i = 0; i < Model.Files.Count; i++) { var file = Model.Files[i]; - if (file.MediaType == MediaType.Video) continue; var attrName = (i == Model.GalleryStartIndex ? "src" : "data-lazy"); var attrValue = file.GetUrl(Model.ImageSize); var srcAttributes = new AttributeDictionary { [attrName] = attrValue }; @@ -73,12 +75,25 @@ data-thumb-image="@file.GetUrl(Model.ThumbSize)" data-medium-image="@file.GetUrl(Model.ImageSize)" data-picture-id="@file.Id"> - @file.Alt + @if (file.MediaType == MediaType.Image) + { + @file.Alt + } + else + { +
+ + @file.Alt + +
+ }
} @@ -93,51 +108,6 @@ }
- - -
-
- @for (int i = 0; i < Model.Files.Count; i++) - { - var file = Model.Files[i]; - var isVideo = file.MediaType == MediaType.Video; - if (!isVideo) continue; - var attrName = (i == Model.GalleryStartIndex ? "src" : "data-lazy"); - var attrValue = file.GetUrl(Model.ImageSize); - var srcAttributes = new AttributeDictionary { [attrName] = attrValue }; - -
- -
- } -
-
- - - -
-
- @for (int i = 0; i < Model.Files.Count; i++) - { - var file = Model.Files[i]; - var isVideo = file.MediaType == MediaType.Video; - if (!isVideo) continue; - var attrName = (i == Model.GalleryStartIndex ? "src" : "data-lazy"); - var attrValue = file.GetUrl(Model.ImageSize); - var srcAttributes = new AttributeDictionary { [attrName] = attrValue }; - -
- - @* *@ -
- } -