Protect distribution APIs with permissions and scope checks

This commit is contained in:
2026-08-18 15:44:10 +05:30
parent be51351f74
commit 42f709d6ee
8 changed files with 99 additions and 12 deletions
+14 -3
View File
@@ -1,16 +1,20 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Order; namespace Distribution.Features.Order;
[ApiController] [ApiController]
[Route("api/orders")] [Route("api/orders")]
[Authorize]
public class OrderController(OrderService service) : ControllerBase public class OrderController(OrderService service) : ControllerBase
{ {
[HttpGet] [HttpGet]
[Authorize(Policy = "orders.view")]
public async Task<IActionResult> GetAll() => public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync()); Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
[Authorize(Policy = "orders.view")]
public async Task<IActionResult> GetById(Guid id) public async Task<IActionResult> GetById(Guid id)
{ {
var result = await service.GetByIdAsync(id); var result = await service.GetByIdAsync(id);
@@ -18,17 +22,23 @@ public class OrderController(OrderService service) : ControllerBase
} }
[HttpGet("store/{storeId:guid}")] [HttpGet("store/{storeId:guid}")]
public async Task<IActionResult> GetByStore(Guid storeId) => [Authorize(Policy = "orders.view")]
Ok(await service.GetByStoreIdAsync(storeId)); public async Task<IActionResult> GetByStore(Guid storeId)
{
var result = await service.GetByStoreIdAsync(storeId);
return result is null ? Forbid() : Ok(result);
}
[HttpPost] [HttpPost]
[Authorize(Policy = "orders.create")]
public async Task<IActionResult> Create(CreateOrderRequest request, CancellationToken ct) public async Task<IActionResult> Create(CreateOrderRequest request, CancellationToken ct)
{ {
var result = await service.CreateAsync(request, 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}")] [HttpPut("{id:guid}")]
[Authorize(Policy = "orders.update")]
public async Task<IActionResult> Update(Guid id, UpdateOrderRequest request, CancellationToken ct) public async Task<IActionResult> Update(Guid id, UpdateOrderRequest request, CancellationToken ct)
{ {
var result = await service.UpdateAsync(id, request, ct); var result = await service.UpdateAsync(id, request, ct);
@@ -36,6 +46,7 @@ public class OrderController(OrderService service) : ControllerBase
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Policy = "orders.delete")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{ {
var deleted = await service.DeleteAsync(id, ct); var deleted = await service.DeleteAsync(id, ct);
+25 -3
View File
@@ -1,17 +1,33 @@
using Distribution.Infra.Auth;
using Distribution.Infra.UoW; using Distribution.Infra.UoW;
namespace Distribution.Features.Order; 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<Models.OrderItem> items) => private static OrderResponse ToResponse(Models.Order o, List<Models.OrderItem> items) =>
new(o.Id, o.OrderDate, o.FK_StoreId, o.TotalAmount, new(o.Id, o.OrderDate, o.FK_StoreId, o.TotalAmount,
items.Select(i => new OrderItemResponse(i.FK_ItemId, i.Quantity)).ToList(), items.Select(i => new OrderItemResponse(i.FK_ItemId, i.Quantity)).ToList(),
o.IsActive, o.CreatedAt); o.IsActive, o.CreatedAt);
private bool HasGlobalScope => currentUser.RoleLevel >= GlobalScopeRoleLevel;
private async Task<bool> HasStoreScopeAsync(Guid storeId)
{
if (HasGlobalScope || currentUser.UserId is null) return HasGlobalScope;
return await scopeAccess.HasStoreScopeAsync(currentUser.UserId.Value, storeId);
}
public async Task<List<OrderResponse>> GetAllAsync() public async Task<List<OrderResponse>> GetAllAsync()
{ {
var orders = (await repo.GetAllAsync()).Where(o => o.IsActive).ToList(); 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<OrderResponse>(); var result = new List<OrderResponse>();
foreach (var order in orders) foreach (var order in orders)
{ {
@@ -25,12 +41,14 @@ public class OrderService(IOrderRepository repo, IUnitOfWork uow)
{ {
var order = await repo.GetByIdAsync(id); var order = await repo.GetByIdAsync(id);
if (order is null || !order.IsActive) return null; if (order is null || !order.IsActive) return null;
if (!await HasStoreScopeAsync(order.FK_StoreId)) return null;
var items = await repo.GetItemsByOrderIdAsync(id); var items = await repo.GetItemsByOrderIdAsync(id);
return ToResponse(order, items); return ToResponse(order, items);
} }
public async Task<List<OrderResponse>> GetByStoreIdAsync(Guid storeId) public async Task<List<OrderResponse>?> GetByStoreIdAsync(Guid storeId)
{ {
if (!await HasStoreScopeAsync(storeId)) return null;
var orders = await repo.GetByStoreIdAsync(storeId); var orders = await repo.GetByStoreIdAsync(storeId);
var result = new List<OrderResponse>(); var result = new List<OrderResponse>();
foreach (var order in orders) foreach (var order in orders)
@@ -41,8 +59,10 @@ public class OrderService(IOrderRepository repo, IUnitOfWork uow)
return result; return result;
} }
public async Task<OrderResponse> CreateAsync(CreateOrderRequest request, CancellationToken ct = default) public async Task<OrderResponse?> CreateAsync(CreateOrderRequest request, CancellationToken ct = default)
{ {
if (!await HasStoreScopeAsync(request.StoreId)) return null;
var order = new Models.Order var order = new Models.Order
{ {
FK_StoreId = request.StoreId, FK_StoreId = request.StoreId,
@@ -75,6 +95,7 @@ public class OrderService(IOrderRepository repo, IUnitOfWork uow)
{ {
var order = await repo.GetByIdAsync(id); var order = await repo.GetByIdAsync(id);
if (order is null || !order.IsActive) return null; if (order is null || !order.IsActive) return null;
if (!await HasStoreScopeAsync(order.FK_StoreId)) return null;
order.OrderDate = request.OrderDate; order.OrderDate = request.OrderDate;
order.TotalAmount = request.TotalAmount; order.TotalAmount = request.TotalAmount;
await uow.BeginAsync(ct); await uow.BeginAsync(ct);
@@ -96,6 +117,7 @@ public class OrderService(IOrderRepository repo, IUnitOfWork uow)
{ {
var order = await repo.GetByIdAsync(id); var order = await repo.GetByIdAsync(id);
if (order is null || !order.IsActive) return false; if (order is null || !order.IsActive) return false;
if (!await HasStoreScopeAsync(order.FK_StoreId)) return false;
await uow.BeginAsync(ct); await uow.BeginAsync(ct);
try try
{ {
@@ -1,16 +1,20 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Region; namespace Distribution.Features.Region;
[ApiController] [ApiController]
[Route("api/regions")] [Route("api/regions")]
[Authorize]
public class RegionController(RegionService service) : ControllerBase public class RegionController(RegionService service) : ControllerBase
{ {
[HttpGet] [HttpGet]
[Authorize(Policy = "regions.view")]
public async Task<IActionResult> GetAll() => public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync()); Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
[Authorize(Policy = "regions.view")]
public async Task<IActionResult> GetById(Guid id) public async Task<IActionResult> GetById(Guid id)
{ {
var result = await service.GetByIdAsync(id); var result = await service.GetByIdAsync(id);
@@ -18,6 +22,7 @@ public class RegionController(RegionService service) : ControllerBase
} }
[HttpPost] [HttpPost]
[Authorize(Policy = "regions.create")]
public async Task<IActionResult> Create(CreateRegionRequest request, CancellationToken ct) public async Task<IActionResult> Create(CreateRegionRequest request, CancellationToken ct)
{ {
var result = await service.CreateAsync(request, ct); var result = await service.CreateAsync(request, ct);
@@ -25,6 +30,7 @@ public class RegionController(RegionService service) : ControllerBase
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Policy = "regions.update")]
public async Task<IActionResult> Update(Guid id, UpdateRegionRequest request, CancellationToken ct) public async Task<IActionResult> Update(Guid id, UpdateRegionRequest request, CancellationToken ct)
{ {
var result = await service.UpdateAsync(id, request, ct); var result = await service.UpdateAsync(id, request, ct);
@@ -32,6 +38,7 @@ public class RegionController(RegionService service) : ControllerBase
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Policy = "regions.delete")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{ {
var deleted = await service.DeleteAsync(id, ct); var deleted = await service.DeleteAsync(id, ct);
@@ -1,16 +1,20 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Route; namespace Distribution.Features.Route;
[ApiController] [ApiController]
[Route("api/routes")] [Route("api/routes")]
[Authorize]
public class RouteController(RouteService service) : ControllerBase public class RouteController(RouteService service) : ControllerBase
{ {
[HttpGet] [HttpGet]
[Authorize(Policy = "routes.view")]
public async Task<IActionResult> GetAll() => public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync()); Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
[Authorize(Policy = "routes.view")]
public async Task<IActionResult> GetById(Guid id) public async Task<IActionResult> GetById(Guid id)
{ {
var result = await service.GetByIdAsync(id); var result = await service.GetByIdAsync(id);
@@ -18,6 +22,7 @@ public class RouteController(RouteService service) : ControllerBase
} }
[HttpPost] [HttpPost]
[Authorize(Policy = "routes.create")]
public async Task<IActionResult> Create(CreateRouteRequest request, CancellationToken ct) public async Task<IActionResult> Create(CreateRouteRequest request, CancellationToken ct)
{ {
var result = await service.CreateAsync(request, ct); var result = await service.CreateAsync(request, ct);
@@ -25,6 +30,7 @@ public class RouteController(RouteService service) : ControllerBase
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Policy = "routes.update")]
public async Task<IActionResult> Update(Guid id, UpdateRouteRequest request, CancellationToken ct) public async Task<IActionResult> Update(Guid id, UpdateRouteRequest request, CancellationToken ct)
{ {
var result = await service.UpdateAsync(id, request, ct); var result = await service.UpdateAsync(id, request, ct);
@@ -32,6 +38,7 @@ public class RouteController(RouteService service) : ControllerBase
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Policy = "routes.delete")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{ {
var deleted = await service.DeleteAsync(id, ct); var deleted = await service.DeleteAsync(id, ct);
@@ -1,16 +1,20 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Store; namespace Distribution.Features.Store;
[ApiController] [ApiController]
[Route("api/stores")] [Route("api/stores")]
[Authorize]
public class StoreController(StoreService service) : ControllerBase public class StoreController(StoreService service) : ControllerBase
{ {
[HttpGet] [HttpGet]
[Authorize(Policy = "stores.view")]
public async Task<IActionResult> GetAll() => public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync()); Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
[Authorize(Policy = "stores.view")]
public async Task<IActionResult> GetById(Guid id) public async Task<IActionResult> GetById(Guid id)
{ {
var result = await service.GetByIdAsync(id); var result = await service.GetByIdAsync(id);
@@ -18,6 +22,7 @@ public class StoreController(StoreService service) : ControllerBase
} }
[HttpPost] [HttpPost]
[Authorize(Policy = "stores.create")]
public async Task<IActionResult> Create(CreateStoreRequest request, CancellationToken ct) public async Task<IActionResult> Create(CreateStoreRequest request, CancellationToken ct)
{ {
var result = await service.CreateAsync(request, ct); var result = await service.CreateAsync(request, ct);
@@ -25,6 +30,7 @@ public class StoreController(StoreService service) : ControllerBase
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Policy = "stores.update")]
public async Task<IActionResult> Update(Guid id, UpdateStoreRequest request, CancellationToken ct) public async Task<IActionResult> Update(Guid id, UpdateStoreRequest request, CancellationToken ct)
{ {
var result = await service.UpdateAsync(id, request, ct); var result = await service.UpdateAsync(id, request, ct);
@@ -32,6 +38,7 @@ public class StoreController(StoreService service) : ControllerBase
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Policy = "stores.delete")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{ {
var deleted = await service.DeleteAsync(id, ct); var deleted = await service.DeleteAsync(id, ct);
+25 -6
View File
@@ -1,22 +1,39 @@
using Distribution.Infra.Auth;
using Distribution.Infra.UoW; using Distribution.Infra.UoW;
namespace Distribution.Features.Store; 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) => 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); new(s.Id, s.OwnerName, s.StoreName, s.StoreAddress, s.Latitude, s.Longitude, s.OneTimeAmount, s.CreditAmount, s.IsActive, s.CreatedAt);
public async Task<List<StoreResponse>> GetAllAsync() => private bool HasGlobalScope => currentUser.RoleLevel >= GlobalScopeRoleLevel;
(await repo.GetAllAsync())
.Where(s => s.IsActive) private async Task<bool> HasStoreScopeAsync(Guid storeId)
.Select(ToResponse) {
.ToList(); if (HasGlobalScope || currentUser.UserId is null) return HasGlobalScope;
return await scopeAccess.HasStoreScopeAsync(currentUser.UserId.Value, storeId);
}
public async Task<List<StoreResponse>> 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<StoreResponse?> GetByIdAsync(Guid id) public async Task<StoreResponse?> GetByIdAsync(Guid id)
{ {
var store = await repo.GetByIdAsync(id); var store = await repo.GetByIdAsync(id);
if (store is null || !store.IsActive) return null; if (store is null || !store.IsActive) return null;
if (!await HasStoreScopeAsync(id)) return null;
return ToResponse(store); return ToResponse(store);
} }
@@ -50,6 +67,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow)
{ {
var store = await repo.GetByIdAsync(id); var store = await repo.GetByIdAsync(id);
if (store is null || !store.IsActive) return null; if (store is null || !store.IsActive) return null;
if (!await HasStoreScopeAsync(id)) return null;
store.OwnerName = request.OwnerName; store.OwnerName = request.OwnerName;
store.StoreName = request.StoreName; store.StoreName = request.StoreName;
store.StoreAddress = request.StoreAddress; store.StoreAddress = request.StoreAddress;
@@ -75,6 +93,7 @@ public class StoreService(IStoreRepository repo, IUnitOfWork uow)
{ {
var store = await repo.GetByIdAsync(id); var store = await repo.GetByIdAsync(id);
if (store is null || !store.IsActive) return false; if (store is null || !store.IsActive) return false;
if (!await HasStoreScopeAsync(id)) return false;
await uow.BeginAsync(ct); await uow.BeginAsync(ct);
try try
{ {
@@ -1,16 +1,20 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Vehicle; namespace Distribution.Features.Vehicle;
[ApiController] [ApiController]
[Route("api/vehicles")] [Route("api/vehicles")]
[Authorize]
public class VehicleController(VehicleService service) : ControllerBase public class VehicleController(VehicleService service) : ControllerBase
{ {
[HttpGet] [HttpGet]
[Authorize(Policy = "vehicles.view")]
public async Task<IActionResult> GetAll() => public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync()); Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
[Authorize(Policy = "vehicles.view")]
public async Task<IActionResult> GetById(Guid id) public async Task<IActionResult> GetById(Guid id)
{ {
var result = await service.GetByIdAsync(id); var result = await service.GetByIdAsync(id);
@@ -18,6 +22,7 @@ public class VehicleController(VehicleService service) : ControllerBase
} }
[HttpPost] [HttpPost]
[Authorize(Policy = "vehicles.create")]
public async Task<IActionResult> Create(CreateVehicleRequest request, CancellationToken ct) public async Task<IActionResult> Create(CreateVehicleRequest request, CancellationToken ct)
{ {
var result = await service.CreateAsync(request, ct); var result = await service.CreateAsync(request, ct);
@@ -25,6 +30,7 @@ public class VehicleController(VehicleService service) : ControllerBase
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Policy = "vehicles.update")]
public async Task<IActionResult> Update(Guid id, UpdateVehicleRequest request, CancellationToken ct) public async Task<IActionResult> Update(Guid id, UpdateVehicleRequest request, CancellationToken ct)
{ {
var result = await service.UpdateAsync(id, request, ct); var result = await service.UpdateAsync(id, request, ct);
@@ -32,6 +38,7 @@ public class VehicleController(VehicleService service) : ControllerBase
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Policy = "vehicles.delete")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{ {
var deleted = await service.DeleteAsync(id, ct); var deleted = await service.DeleteAsync(id, ct);
@@ -1,16 +1,20 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Distribution.Features.Warehouse; namespace Distribution.Features.Warehouse;
[ApiController] [ApiController]
[Route("api/warehouses")] [Route("api/warehouses")]
[Authorize]
public class WarehouseController(WarehouseService service) : ControllerBase public class WarehouseController(WarehouseService service) : ControllerBase
{ {
[HttpGet] [HttpGet]
[Authorize(Policy = "warehouses.view")]
public async Task<IActionResult> GetAll() => public async Task<IActionResult> GetAll() =>
Ok(await service.GetAllAsync()); Ok(await service.GetAllAsync());
[HttpGet("{id:guid}")] [HttpGet("{id:guid}")]
[Authorize(Policy = "warehouses.view")]
public async Task<IActionResult> GetById(Guid id) public async Task<IActionResult> GetById(Guid id)
{ {
var result = await service.GetByIdAsync(id); var result = await service.GetByIdAsync(id);
@@ -18,6 +22,7 @@ public class WarehouseController(WarehouseService service) : ControllerBase
} }
[HttpPost] [HttpPost]
[Authorize(Policy = "warehouses.create")]
public async Task<IActionResult> Create(CreateWarehouseRequest request, CancellationToken ct) public async Task<IActionResult> Create(CreateWarehouseRequest request, CancellationToken ct)
{ {
var result = await service.CreateAsync(request, ct); var result = await service.CreateAsync(request, ct);
@@ -25,6 +30,7 @@ public class WarehouseController(WarehouseService service) : ControllerBase
} }
[HttpPut("{id:guid}")] [HttpPut("{id:guid}")]
[Authorize(Policy = "warehouses.update")]
public async Task<IActionResult> Update(Guid id, UpdateWarehouseRequest request, CancellationToken ct) public async Task<IActionResult> Update(Guid id, UpdateWarehouseRequest request, CancellationToken ct)
{ {
var result = await service.UpdateAsync(id, request, ct); var result = await service.UpdateAsync(id, request, ct);
@@ -32,6 +38,7 @@ public class WarehouseController(WarehouseService service) : ControllerBase
} }
[HttpDelete("{id:guid}")] [HttpDelete("{id:guid}")]
[Authorize(Policy = "warehouses.delete")]
public async Task<IActionResult> Delete(Guid id, CancellationToken ct) public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
{ {
var deleted = await service.DeleteAsync(id, ct); var deleted = await service.DeleteAsync(id, ct);