From 42f709d6ee8ae6590655175ca9044e37b3765639 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Tue, 18 Aug 2026 15:44:10 +0530 Subject: [PATCH] Protect distribution APIs with permissions and scope checks --- .../Features/Order/OrderController.cs | 17 ++++++++-- Distribution/Features/Order/OrderService.cs | 28 +++++++++++++++-- .../Features/Region/RegionController.cs | 7 +++++ .../Features/Route/RouteController.cs | 7 +++++ .../Features/Store/StoreController.cs | 7 +++++ Distribution/Features/Store/StoreService.cs | 31 +++++++++++++++---- .../Features/Vehicle/VehicleController.cs | 7 +++++ .../Features/Warehouse/WarehouseController.cs | 7 +++++ 8 files changed, 99 insertions(+), 12 deletions(-) diff --git a/Distribution/Features/Order/OrderController.cs b/Distribution/Features/Order/OrderController.cs index 4bfe01c..4aacb79 100644 --- a/Distribution/Features/Order/OrderController.cs +++ b/Distribution/Features/Order/OrderController.cs @@ -1,16 +1,20 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Distribution.Features.Order; [ApiController] [Route("api/orders")] +[Authorize] public class OrderController(OrderService service) : ControllerBase { [HttpGet] + [Authorize(Policy = "orders.view")] public async Task GetAll() => Ok(await service.GetAllAsync()); [HttpGet("{id:guid}")] + [Authorize(Policy = "orders.view")] public async Task GetById(Guid id) { var result = await service.GetByIdAsync(id); @@ -18,17 +22,23 @@ public class OrderController(OrderService service) : ControllerBase } [HttpGet("store/{storeId:guid}")] - public async Task GetByStore(Guid storeId) => - Ok(await service.GetByStoreIdAsync(storeId)); + [Authorize(Policy = "orders.view")] + public async Task GetByStore(Guid storeId) + { + var result = await service.GetByStoreIdAsync(storeId); + return result is null ? Forbid() : Ok(result); + } [HttpPost] + [Authorize(Policy = "orders.create")] public async Task Create(CreateOrderRequest request, CancellationToken ct) { var result = await service.CreateAsync(request, ct); - return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); + return result is null ? Forbid() : CreatedAtAction(nameof(GetById), new { id = result.Id }, result); } [HttpPut("{id:guid}")] + [Authorize(Policy = "orders.update")] public async Task Update(Guid id, UpdateOrderRequest request, CancellationToken ct) { var result = await service.UpdateAsync(id, request, ct); @@ -36,6 +46,7 @@ public class OrderController(OrderService service) : ControllerBase } [HttpDelete("{id:guid}")] + [Authorize(Policy = "orders.delete")] public async Task Delete(Guid id, CancellationToken ct) { var deleted = await service.DeleteAsync(id, ct); diff --git a/Distribution/Features/Order/OrderService.cs b/Distribution/Features/Order/OrderService.cs index 993c2ce..16c9999 100644 --- a/Distribution/Features/Order/OrderService.cs +++ b/Distribution/Features/Order/OrderService.cs @@ -1,17 +1,33 @@ +using Distribution.Infra.Auth; using Distribution.Infra.UoW; namespace Distribution.Features.Order; -public class OrderService(IOrderRepository repo, IUnitOfWork uow) +public class OrderService(IOrderRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess) { + private const int GlobalScopeRoleLevel = 50; + private static OrderResponse ToResponse(Models.Order o, List items) => new(o.Id, o.OrderDate, o.FK_StoreId, o.TotalAmount, items.Select(i => new OrderItemResponse(i.FK_ItemId, i.Quantity)).ToList(), o.IsActive, o.CreatedAt); + private bool HasGlobalScope => currentUser.RoleLevel >= GlobalScopeRoleLevel; + + private async Task HasStoreScopeAsync(Guid storeId) + { + if (HasGlobalScope || currentUser.UserId is null) return HasGlobalScope; + return await scopeAccess.HasStoreScopeAsync(currentUser.UserId.Value, storeId); + } + public async Task> GetAllAsync() { var orders = (await repo.GetAllAsync()).Where(o => o.IsActive).ToList(); + if (!HasGlobalScope && currentUser.UserId is not null) + { + var accessibleStoreIds = (await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value)).ToHashSet(); + orders = orders.Where(o => accessibleStoreIds.Contains(o.FK_StoreId)).ToList(); + } var result = new List(); foreach (var order in orders) { @@ -25,12 +41,14 @@ public class OrderService(IOrderRepository repo, IUnitOfWork uow) { var order = await repo.GetByIdAsync(id); if (order is null || !order.IsActive) return null; + if (!await HasStoreScopeAsync(order.FK_StoreId)) return null; var items = await repo.GetItemsByOrderIdAsync(id); return ToResponse(order, items); } - public async Task> GetByStoreIdAsync(Guid storeId) + public async Task?> GetByStoreIdAsync(Guid storeId) { + if (!await HasStoreScopeAsync(storeId)) return null; var orders = await repo.GetByStoreIdAsync(storeId); var result = new List(); foreach (var order in orders) @@ -41,8 +59,10 @@ public class OrderService(IOrderRepository repo, IUnitOfWork uow) return result; } - public async Task CreateAsync(CreateOrderRequest request, CancellationToken ct = default) + public async Task CreateAsync(CreateOrderRequest request, CancellationToken ct = default) { + if (!await HasStoreScopeAsync(request.StoreId)) return null; + var order = new Models.Order { FK_StoreId = request.StoreId, @@ -75,6 +95,7 @@ public class OrderService(IOrderRepository repo, IUnitOfWork uow) { var order = await repo.GetByIdAsync(id); if (order is null || !order.IsActive) return null; + if (!await HasStoreScopeAsync(order.FK_StoreId)) return null; order.OrderDate = request.OrderDate; order.TotalAmount = request.TotalAmount; await uow.BeginAsync(ct); @@ -96,6 +117,7 @@ public class OrderService(IOrderRepository repo, IUnitOfWork uow) { var order = await repo.GetByIdAsync(id); if (order is null || !order.IsActive) return false; + if (!await HasStoreScopeAsync(order.FK_StoreId)) return false; await uow.BeginAsync(ct); try { diff --git a/Distribution/Features/Region/RegionController.cs b/Distribution/Features/Region/RegionController.cs index 42ab2bc..e085c52 100644 --- a/Distribution/Features/Region/RegionController.cs +++ b/Distribution/Features/Region/RegionController.cs @@ -1,16 +1,20 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Distribution.Features.Region; [ApiController] [Route("api/regions")] +[Authorize] public class RegionController(RegionService service) : ControllerBase { [HttpGet] + [Authorize(Policy = "regions.view")] public async Task GetAll() => Ok(await service.GetAllAsync()); [HttpGet("{id:guid}")] + [Authorize(Policy = "regions.view")] public async Task GetById(Guid id) { var result = await service.GetByIdAsync(id); @@ -18,6 +22,7 @@ public class RegionController(RegionService service) : ControllerBase } [HttpPost] + [Authorize(Policy = "regions.create")] public async Task Create(CreateRegionRequest request, CancellationToken ct) { var result = await service.CreateAsync(request, ct); @@ -25,6 +30,7 @@ public class RegionController(RegionService service) : ControllerBase } [HttpPut("{id:guid}")] + [Authorize(Policy = "regions.update")] public async Task Update(Guid id, UpdateRegionRequest request, CancellationToken ct) { var result = await service.UpdateAsync(id, request, ct); @@ -32,6 +38,7 @@ public class RegionController(RegionService service) : ControllerBase } [HttpDelete("{id:guid}")] + [Authorize(Policy = "regions.delete")] public async Task Delete(Guid id, CancellationToken ct) { var deleted = await service.DeleteAsync(id, ct); diff --git a/Distribution/Features/Route/RouteController.cs b/Distribution/Features/Route/RouteController.cs index afa13a6..9789613 100644 --- a/Distribution/Features/Route/RouteController.cs +++ b/Distribution/Features/Route/RouteController.cs @@ -1,16 +1,20 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Distribution.Features.Route; [ApiController] [Route("api/routes")] +[Authorize] public class RouteController(RouteService service) : ControllerBase { [HttpGet] + [Authorize(Policy = "routes.view")] public async Task GetAll() => Ok(await service.GetAllAsync()); [HttpGet("{id:guid}")] + [Authorize(Policy = "routes.view")] public async Task GetById(Guid id) { var result = await service.GetByIdAsync(id); @@ -18,6 +22,7 @@ public class RouteController(RouteService service) : ControllerBase } [HttpPost] + [Authorize(Policy = "routes.create")] public async Task Create(CreateRouteRequest request, CancellationToken ct) { var result = await service.CreateAsync(request, ct); @@ -25,6 +30,7 @@ public class RouteController(RouteService service) : ControllerBase } [HttpPut("{id:guid}")] + [Authorize(Policy = "routes.update")] public async Task Update(Guid id, UpdateRouteRequest request, CancellationToken ct) { var result = await service.UpdateAsync(id, request, ct); @@ -32,6 +38,7 @@ public class RouteController(RouteService service) : ControllerBase } [HttpDelete("{id:guid}")] + [Authorize(Policy = "routes.delete")] public async Task Delete(Guid id, CancellationToken ct) { var deleted = await service.DeleteAsync(id, ct); diff --git a/Distribution/Features/Store/StoreController.cs b/Distribution/Features/Store/StoreController.cs index c628405..9cfb5a9 100644 --- a/Distribution/Features/Store/StoreController.cs +++ b/Distribution/Features/Store/StoreController.cs @@ -1,16 +1,20 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Distribution.Features.Store; [ApiController] [Route("api/stores")] +[Authorize] public class StoreController(StoreService service) : ControllerBase { [HttpGet] + [Authorize(Policy = "stores.view")] public async Task GetAll() => Ok(await service.GetAllAsync()); [HttpGet("{id:guid}")] + [Authorize(Policy = "stores.view")] public async Task GetById(Guid id) { var result = await service.GetByIdAsync(id); @@ -18,6 +22,7 @@ public class StoreController(StoreService service) : ControllerBase } [HttpPost] + [Authorize(Policy = "stores.create")] public async Task Create(CreateStoreRequest request, CancellationToken ct) { var result = await service.CreateAsync(request, ct); @@ -25,6 +30,7 @@ public class StoreController(StoreService service) : ControllerBase } [HttpPut("{id:guid}")] + [Authorize(Policy = "stores.update")] public async Task Update(Guid id, UpdateStoreRequest request, CancellationToken ct) { var result = await service.UpdateAsync(id, request, ct); @@ -32,6 +38,7 @@ public class StoreController(StoreService service) : ControllerBase } [HttpDelete("{id:guid}")] + [Authorize(Policy = "stores.delete")] public async Task Delete(Guid id, CancellationToken ct) { var deleted = await service.DeleteAsync(id, ct); diff --git a/Distribution/Features/Store/StoreService.cs b/Distribution/Features/Store/StoreService.cs index c4c4d4b..6f6fb32 100644 --- a/Distribution/Features/Store/StoreService.cs +++ b/Distribution/Features/Store/StoreService.cs @@ -1,22 +1,39 @@ +using Distribution.Infra.Auth; using Distribution.Infra.UoW; namespace Distribution.Features.Store; -public class StoreService(IStoreRepository repo, IUnitOfWork uow) +public class StoreService(IStoreRepository repo, IUnitOfWork uow, ICurrentUserService currentUser, IScopeAccessService scopeAccess) { + private const int GlobalScopeRoleLevel = 50; + private static StoreResponse ToResponse(Models.Store s) => new(s.Id, s.OwnerName, s.StoreName, s.StoreAddress, s.Latitude, s.Longitude, s.OneTimeAmount, s.CreditAmount, s.IsActive, s.CreatedAt); - public async Task> GetAllAsync() => - (await repo.GetAllAsync()) - .Where(s => s.IsActive) - .Select(ToResponse) - .ToList(); + private bool HasGlobalScope => currentUser.RoleLevel >= GlobalScopeRoleLevel; + + private async Task HasStoreScopeAsync(Guid storeId) + { + if (HasGlobalScope || currentUser.UserId is null) return HasGlobalScope; + return await scopeAccess.HasStoreScopeAsync(currentUser.UserId.Value, storeId); + } + + public async Task> GetAllAsync() + { + var stores = (await repo.GetAllAsync()).Where(s => s.IsActive).ToList(); + if (!HasGlobalScope && currentUser.UserId is not null) + { + var accessibleStoreIds = (await scopeAccess.GetAccessibleStoreIdsAsync(currentUser.UserId.Value)).ToHashSet(); + stores = stores.Where(s => accessibleStoreIds.Contains(s.Id)).ToList(); + } + return stores.Select(ToResponse).ToList(); + } public async Task GetByIdAsync(Guid id) { var store = await repo.GetByIdAsync(id); if (store is null || !store.IsActive) return null; + if (!await HasStoreScopeAsync(id)) return null; return ToResponse(store); } @@ -50,6 +67,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow) { var store = await repo.GetByIdAsync(id); if (store is null || !store.IsActive) return null; + if (!await HasStoreScopeAsync(id)) return null; store.OwnerName = request.OwnerName; store.StoreName = request.StoreName; store.StoreAddress = request.StoreAddress; @@ -75,6 +93,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow) { var store = await repo.GetByIdAsync(id); if (store is null || !store.IsActive) return false; + if (!await HasStoreScopeAsync(id)) return false; await uow.BeginAsync(ct); try { diff --git a/Distribution/Features/Vehicle/VehicleController.cs b/Distribution/Features/Vehicle/VehicleController.cs index 2d80ecd..3dabed5 100644 --- a/Distribution/Features/Vehicle/VehicleController.cs +++ b/Distribution/Features/Vehicle/VehicleController.cs @@ -1,16 +1,20 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Distribution.Features.Vehicle; [ApiController] [Route("api/vehicles")] +[Authorize] public class VehicleController(VehicleService service) : ControllerBase { [HttpGet] + [Authorize(Policy = "vehicles.view")] public async Task GetAll() => Ok(await service.GetAllAsync()); [HttpGet("{id:guid}")] + [Authorize(Policy = "vehicles.view")] public async Task GetById(Guid id) { var result = await service.GetByIdAsync(id); @@ -18,6 +22,7 @@ public class VehicleController(VehicleService service) : ControllerBase } [HttpPost] + [Authorize(Policy = "vehicles.create")] public async Task Create(CreateVehicleRequest request, CancellationToken ct) { var result = await service.CreateAsync(request, ct); @@ -25,6 +30,7 @@ public class VehicleController(VehicleService service) : ControllerBase } [HttpPut("{id:guid}")] + [Authorize(Policy = "vehicles.update")] public async Task Update(Guid id, UpdateVehicleRequest request, CancellationToken ct) { var result = await service.UpdateAsync(id, request, ct); @@ -32,6 +38,7 @@ public class VehicleController(VehicleService service) : ControllerBase } [HttpDelete("{id:guid}")] + [Authorize(Policy = "vehicles.delete")] public async Task Delete(Guid id, CancellationToken ct) { var deleted = await service.DeleteAsync(id, ct); diff --git a/Distribution/Features/Warehouse/WarehouseController.cs b/Distribution/Features/Warehouse/WarehouseController.cs index 44d8cdd..bcc8464 100644 --- a/Distribution/Features/Warehouse/WarehouseController.cs +++ b/Distribution/Features/Warehouse/WarehouseController.cs @@ -1,16 +1,20 @@ +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace Distribution.Features.Warehouse; [ApiController] [Route("api/warehouses")] +[Authorize] public class WarehouseController(WarehouseService service) : ControllerBase { [HttpGet] + [Authorize(Policy = "warehouses.view")] public async Task GetAll() => Ok(await service.GetAllAsync()); [HttpGet("{id:guid}")] + [Authorize(Policy = "warehouses.view")] public async Task GetById(Guid id) { var result = await service.GetByIdAsync(id); @@ -18,6 +22,7 @@ public class WarehouseController(WarehouseService service) : ControllerBase } [HttpPost] + [Authorize(Policy = "warehouses.create")] public async Task Create(CreateWarehouseRequest request, CancellationToken ct) { var result = await service.CreateAsync(request, ct); @@ -25,6 +30,7 @@ public class WarehouseController(WarehouseService service) : ControllerBase } [HttpPut("{id:guid}")] + [Authorize(Policy = "warehouses.update")] public async Task Update(Guid id, UpdateWarehouseRequest request, CancellationToken ct) { var result = await service.UpdateAsync(id, request, ct); @@ -32,6 +38,7 @@ public class WarehouseController(WarehouseService service) : ControllerBase } [HttpDelete("{id:guid}")] + [Authorize(Policy = "warehouses.delete")] public async Task Delete(Guid id, CancellationToken ct) { var deleted = await service.DeleteAsync(id, ct);