forked from xiongyuxing/tiku-backend.net
feat: harden SaaS authentication and authorization
This commit is contained in:
@@ -3,18 +3,21 @@ using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Tiku.Application.Commerce;
|
||||
using Tiku.Application.Security;
|
||||
using Tiku.Application.Tenancy;
|
||||
using Tiku.Domain.Catalog;
|
||||
using Tiku.Domain.Commerce;
|
||||
using Tiku.Domain.Tenancy;
|
||||
using Tiku.Infrastructure.Persistence;
|
||||
using Tiku.Infrastructure.Security;
|
||||
|
||||
namespace Tiku.Infrastructure.Commerce;
|
||||
|
||||
internal sealed class CommerceAdminService(
|
||||
TikuDbContext dbContext,
|
||||
ITenantSecretProtector tenantSecretProtector,
|
||||
ITenantExternalProviderConfigService providerConfigService) : ICommerceAdminService
|
||||
ITenantExternalProviderConfigService providerConfigService,
|
||||
ICurrentAccessContext currentAccessContext) : ICommerceAdminService
|
||||
{
|
||||
public async Task<IReadOnlyCollection<TenantPaymentProviderItem>> GetPaymentAccountsAsync(
|
||||
CommerceAdminActor actor,
|
||||
@@ -105,8 +108,14 @@ internal sealed class CommerceAdminService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
||||
var regionIds = scope.RegionIds.ToArray();
|
||||
var orders = dbContext.Orders.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId);
|
||||
.Where(item => item.TenantId == actor.TenantId)
|
||||
.ApplyDataScope(
|
||||
scope,
|
||||
item => item.UserId == actor.UserId,
|
||||
item => item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value));
|
||||
if (!string.IsNullOrWhiteSpace(query.Status))
|
||||
{
|
||||
orders = orders.Where(item => item.Status == ParseOrderStatus(query.Status));
|
||||
@@ -125,8 +134,16 @@ internal sealed class CommerceAdminService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
||||
var regionIds = scope.RegionIds.ToArray();
|
||||
var scopedOrders = dbContext.Orders.AsNoTracking()
|
||||
.Where(order => order.TenantId == actor.TenantId)
|
||||
.ApplyDataScope(
|
||||
scope,
|
||||
order => order.UserId == actor.UserId,
|
||||
order => order.RegionId.HasValue && regionIds.Contains(order.RegionId.Value));
|
||||
var payments = from payment in dbContext.Payments.AsNoTracking()
|
||||
join order in dbContext.Orders.AsNoTracking()
|
||||
join order in scopedOrders
|
||||
on new { payment.TenantId, payment.OrderId } equals new { order.TenantId, OrderId = order.Id }
|
||||
where payment.TenantId == actor.TenantId
|
||||
select new { payment, order.OrderNo };
|
||||
@@ -568,8 +585,19 @@ internal sealed class CommerceAdminService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
||||
var regionIds = scope.RegionIds.ToArray();
|
||||
var refunds = dbContext.CommerceRefundRequests.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId);
|
||||
.Where(item => item.TenantId == actor.TenantId)
|
||||
.ApplyDataScope(
|
||||
scope,
|
||||
item => item.RequestedBy == actor.UserId || dbContext.Orders.Any(order =>
|
||||
order.TenantId == actor.TenantId && order.Id == item.OrderId && order.UserId == actor.UserId),
|
||||
item => dbContext.Orders.Any(order =>
|
||||
order.TenantId == actor.TenantId &&
|
||||
order.Id == item.OrderId &&
|
||||
order.RegionId.HasValue &&
|
||||
regionIds.Contains(order.RegionId.Value)));
|
||||
if (!string.IsNullOrWhiteSpace(query.Status))
|
||||
{
|
||||
refunds = refunds.Where(item => item.Status == ParseRefundStatus(query.Status));
|
||||
@@ -588,9 +616,16 @@ internal sealed class CommerceAdminService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var order = await dbContext.Orders.SingleOrDefaultAsync(
|
||||
item => item.TenantId == actor.TenantId && item.Id == command.OrderId,
|
||||
cancellationToken) ?? throw new CommerceException("Order was not found.", "order_not_found");
|
||||
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
||||
var regionIds = scope.RegionIds.ToArray();
|
||||
var order = await dbContext.Orders
|
||||
.Where(item => item.TenantId == actor.TenantId && item.Id == command.OrderId)
|
||||
.ApplyDataScope(
|
||||
scope,
|
||||
item => item.UserId == actor.UserId,
|
||||
item => item.RegionId.HasValue && regionIds.Contains(item.RegionId.Value))
|
||||
.SingleOrDefaultAsync(cancellationToken)
|
||||
?? throw new CommerceException("Order was not found.", "order_not_found");
|
||||
if (order.Status is not (OrderStatus.Paid or OrderStatus.PartiallyRefunded))
|
||||
{
|
||||
throw new CommerceException("Only paid orders can be refunded.", "order_not_refundable");
|
||||
@@ -639,9 +674,21 @@ internal sealed class CommerceAdminService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var refund = await dbContext.CommerceRefundRequests.SingleOrDefaultAsync(
|
||||
item => item.TenantId == actor.TenantId && item.Id == command.RefundRequestId,
|
||||
cancellationToken) ?? throw new CommerceException("Refund request was not found.", "refund_not_found");
|
||||
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
||||
var regionIds = scope.RegionIds.ToArray();
|
||||
var refund = await dbContext.CommerceRefundRequests
|
||||
.Where(item => item.TenantId == actor.TenantId && item.Id == command.RefundRequestId)
|
||||
.ApplyDataScope(
|
||||
scope,
|
||||
item => item.RequestedBy == actor.UserId || dbContext.Orders.Any(order =>
|
||||
order.TenantId == actor.TenantId && order.Id == item.OrderId && order.UserId == actor.UserId),
|
||||
item => dbContext.Orders.Any(order =>
|
||||
order.TenantId == actor.TenantId &&
|
||||
order.Id == item.OrderId &&
|
||||
order.RegionId.HasValue &&
|
||||
regionIds.Contains(order.RegionId.Value)))
|
||||
.SingleOrDefaultAsync(cancellationToken)
|
||||
?? throw new CommerceException("Refund request was not found.", "refund_not_found");
|
||||
var fromStatus = refund.Status;
|
||||
if (!IsAllowedRefundTransition(fromStatus, command.Status))
|
||||
{
|
||||
@@ -687,6 +734,25 @@ internal sealed class CommerceAdminService(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
var scope = await RequireDataScopeAsync(actor, cancellationToken);
|
||||
var regionIds = scope.RegionIds.ToArray();
|
||||
var refundExists = await dbContext.CommerceRefundRequests
|
||||
.Where(item => item.TenantId == actor.TenantId && item.Id == refundRequestId)
|
||||
.ApplyDataScope(
|
||||
scope,
|
||||
item => item.RequestedBy == actor.UserId || dbContext.Orders.Any(order =>
|
||||
order.TenantId == actor.TenantId && order.Id == item.OrderId && order.UserId == actor.UserId),
|
||||
item => dbContext.Orders.Any(order =>
|
||||
order.TenantId == actor.TenantId &&
|
||||
order.Id == item.OrderId &&
|
||||
order.RegionId.HasValue &&
|
||||
regionIds.Contains(order.RegionId.Value)))
|
||||
.AnyAsync(cancellationToken);
|
||||
if (!refundExists)
|
||||
{
|
||||
throw new CommerceException("Refund request was not found.", "refund_not_found");
|
||||
}
|
||||
|
||||
var items = await dbContext.CommerceRefundEvents.AsNoTracking()
|
||||
.Where(item => item.TenantId == actor.TenantId && item.RefundRequestId == refundRequestId)
|
||||
.OrderBy(item => item.CreatedAt)
|
||||
@@ -807,20 +873,24 @@ internal sealed class CommerceAdminService(
|
||||
|
||||
private async Task AssertAdminAsync(CommerceAdminActor actor, CancellationToken cancellationToken)
|
||||
{
|
||||
var isAdmin = await dbContext.TenantMemberships.AnyAsync(item =>
|
||||
item.TenantId == actor.TenantId &&
|
||||
item.UserId == actor.UserId &&
|
||||
item.Status == MembershipStatus.Active &&
|
||||
(item.Role == TenantRole.PlatformAdmin ||
|
||||
item.Role == TenantRole.TenantOwner ||
|
||||
item.Role == TenantRole.TenantAdmin),
|
||||
cancellationToken);
|
||||
if (!isAdmin)
|
||||
var access = await currentAccessContext.GetAsync(cancellationToken);
|
||||
if (!access.IsCurrentTenantMember ||
|
||||
access.UserId != actor.UserId ||
|
||||
access.TenantId != actor.TenantId ||
|
||||
!access.HasTenantPermission(BackendPermissions.TenantCommerceOperate))
|
||||
{
|
||||
throw new CommerceException("Tenant admin access is required.", "tenant_admin_access_denied");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<CurrentDataScope> RequireDataScopeAsync(
|
||||
CommerceAdminActor actor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await AssertAdminAsync(actor, cancellationToken);
|
||||
return (await currentAccessContext.GetAsync(cancellationToken)).DataScope;
|
||||
}
|
||||
|
||||
private static TenantPaymentProviderItem ToPaymentAccountItem(TenantExternalProviderItem item) =>
|
||||
new(
|
||||
item.Id,
|
||||
|
||||
Reference in New Issue
Block a user