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/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/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..01738d32f2 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 (_workContext.Value.CurrentCustomer.IsSuperAdmin() || 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.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..89bc537d4e 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 (!_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; + } + 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 _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) { 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 89678a2aac..e9414b7324 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 332f5b20c4..ba065f48c8 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; @@ -179,6 +178,9 @@ public async Task CategoryList(GridCommand command, CategoryListM var categories = await query .ApplyStandardFilter(true, null, model.SearchStoreId) + .ApplyCustomerStoreFilter( + await Services.StoreMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await Services.StoreMappingService.GetStoreMappingCollectionAsync(nameof(Category), [.. query.Select(x => x.Id)])) .ApplyGridCommand(command, false) .ToPagedList(command) .LoadAsync(); @@ -326,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(); @@ -360,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); @@ -408,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); @@ -617,7 +625,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 b539da22c5..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; @@ -59,6 +60,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) @@ -167,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 ab701925b1..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; @@ -48,7 +49,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 +69,7 @@ public CustomerController( Lazy shoppingCartService, Lazy shippingService, Lazy paymentService, - ShoppingCartSettings shoppingCartSettings, - IStoreMappingService storeMappingService) + ShoppingCartSettings shoppingCartSettings) { _db = db; _customerService = customerService; @@ -91,7 +90,6 @@ public CustomerController( _shippingService = shippingService; _paymentService = paymentService; _shoppingCartSettings = shoppingCartSettings; - _storeMappingService = storeMappingService; } #region Utilities @@ -141,7 +139,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) { @@ -226,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) @@ -428,7 +426,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); @@ -444,7 +442,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) { @@ -537,6 +539,13 @@ public async Task Create(CustomerModel model, bool continueEditin LastActivityDateUtc = DateTime.UtcNow }; + // Validate super admin + if (!Services.Permissions.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); @@ -580,7 +589,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)); @@ -620,9 +632,14 @@ 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); } @@ -640,8 +657,9 @@ public async Task Edit(CustomerModel model, bool continueEditing, { return NotFound(); } - - if (customer.IsAdmin() && !Services.WorkContext.CurrentCustomer.IsAdmin()) + + // Validate super admin + if (!Services.Permissions.ValidateSuperAdmin(model.SelectedCustomerRoleIds)) { NotifyAccessDenied(); return RedirectToAction(nameof(Edit), new { customer.Id }); @@ -767,7 +785,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)); @@ -1037,6 +1058,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(); @@ -1342,7 +1367,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(); } @@ -1366,6 +1394,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 d07c3fcc7c..6a685ebd8c 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,9 +129,11 @@ public IActionResult List() [Permission(Permissions.Customer.Role.Read)] public async Task RoleList(GridCommand command) { + var isSuperAdmin = Services.WorkContext.CurrentCustomer.IsSuperAdmin(); var mapper = MapperFactory.GetMapper(); 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 ec7b085ebc..307d4f1df6 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/ManufacturerController.cs @@ -130,9 +130,12 @@ 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( + await _storeMappingService.GetCustomerAuthorizedStoreIdsAsync(), + await _storeMappingService.GetStoreMappingCollectionAsync(nameof(Manufacturer), [.. query.Select(x => x.Id)])) .ApplyGridCommand(command, false) .ToPagedList(command) .LoadAsync(); @@ -240,6 +243,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 545fd926c5..2d3f370357 100644 --- a/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs +++ b/src/Smartstore.Web/Areas/Admin/Controllers/OrderController.cs @@ -167,8 +167,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 @@ -188,7 +186,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) { @@ -878,6 +876,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); @@ -1727,7 +1731,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/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 82345c53e0..d6e17ba9fe 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(); @@ -235,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(); 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..4198e75052 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 (!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(); + } + if (multiple && !attributes.ContainsKey("data-placeholder")) { attributes["data-placeholder"] = T("Admin.Common.StoresAll").Value; diff --git a/src/Smartstore.Web/Views/Product/Product.cshtml b/src/Smartstore.Web/Views/Product/Product.cshtml index 60e3d2bdce..320f501fd3 100644 --- a/src/Smartstore.Web/Views/Product/Product.cshtml +++ b/src/Smartstore.Web/Views/Product/Product.cshtml @@ -18,7 +18,7 @@
- +
@@ -78,9 +78,9 @@ @if (Model.DisplayAdminLink) { - @T("Common.Catalog.EditProduct") @@ -134,7 +134,7 @@ } - + @@ -157,6 +157,6 @@ enableZoom: toBool('@(Model.MediaGalleryModel.ImageZoomEnabled)') }; - $('#pd-form').productDetail(settings); + $('#pd-form').productDetail(settings); }); \ No newline at end of file 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]