{"record":{"id":"3fe095ced60a966c","repo":"fullstackhero/dotnet-starter-kit","slug":"you-can-only-approve-top-up-requests-for-your-own-tenant","errorCode":null,"errorMessage":"You can only approve top-up requests for your own tenant.","messagePattern":"You can only approve top-up requests for your own tenant\\.","errorType":"exception","errorClass":"UnauthorizedException","httpStatus":403,"severity":"error","filePath":"src/Modules/Billing/Modules.Billing/Features/v1/Wallets/ApproveTopupRequest/ApproveTopupRequestCommandHandler.cs","lineNumber":33,"sourceCode":"    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)\n    : ICommandHandler<ApproveTopupRequestCommand, Guid>\n{\n    public async ValueTask<Guid> Handle(ApproveTopupRequestCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id\n            ?? throw new UnauthorizedException(\"Tenant context is required.\");\n        var isRoot = callerTenantId == MultitenancyConstants.Root.Id;\n\n        var request = await db.TopupRequests\n            .FirstOrDefaultAsync(r => r.Id == command.Id, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Top-up request {command.Id} not found.\");\n\n        if (!isRoot && request.TenantId != callerTenantId)\n        {\n            throw new UnauthorizedException(\"You can only approve top-up requests for your own tenant.\");\n        }\n\n        // For root, operate on the request's own tenant; for non-root, callerTenantId equals request.TenantId.\n        var invoice = await billing.CreateTopupInvoiceAsync(request.TenantId, command.Id, cancellationToken)\n            .ConfigureAwait(false);\n\n        return invoice.Id;\n    }\n}\n","sourceCodeStart":15,"sourceCodeEnd":43,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Billing/Modules.Billing/Features/v1/Wallets/ApproveTopupRequest/ApproveTopupRequestCommandHandler.cs#L15-L43","documentation":"This handler enforces tenant ownership manually (BillingDbContext is not tenant-filtered): if the caller is not root and the TopupRequest.TenantId differs from the caller's tenant id, it throws UnauthorizedException('You can only approve top-up requests for your own tenant.'). This prevents a tenant user from approving another tenant's top-up request and generating an invoice for it.","triggerScenarios":"A non-root tenant user approving (or double-approving after tenant reassignment) a request whose TenantId differs from the caller's resolved tenant — e.g. guessing/using another tenant's request id, or approving after the request was created under a different tenant identifier.","commonSituations":"Admins operating with a regular tenant token instead of root while trying to manage other tenants' wallets; multi-environment testing where the same request id is reused across tenants; a tenant user following a stale/deep link to another tenant's request.","solutions":["If cross-tenant approval is intended, authenticate as the root operator (root tenant context) and retry.","Otherwise approve using credentials of the tenant that owns the request (send the request's own tenant id in __tenant__).","Confirm the request's tenant: GET the top-up request and compare TenantId with your token's tenant.","Fix client code that caches request ids across tenant logins/sessions."],"exampleFix":"// before (tenant 'acme' approving request owned by 'globex')\nawait client.PostAsync($\"/api/v1/wallets/topup-requests/{globexRequestId}/approve\", null);\n// after: root operator, or the owning tenant\nclient.DefaultRequestHeaders.Add(\"__tenant__\", \"globex\");\nawait client.PostAsync($\"/api/v1/wallets/topup-requests/{globexRequestId}/approve\", null);","handlingStrategy":"validation","validationCode":"var req = await api.GetTopupRequestAsync(id);\nif (currentUserTenant != \"root\" && req.TenantId != currentUserTenant)\n    throw new InvalidOperationException(\"Cannot approve another tenant's top-up request\");","typeGuard":"bool CanApprove(string callerTenant, TopupRequestDto r) => callerTenant == \"root\" || r.TenantId == callerTenant;","tryCatchPattern":"try { await api.ApproveTopupRequestAsync(id); }\ncatch (UnauthorizedException ex) when (ex.Message.Contains(\"your own tenant\"))\n{\n    // switch to root credentials or drop the operation\n}","preventionTips":["Hide/Disable approve actions in the UI for requests whose TenantId differs from the current tenant.","Use root credentials (root tenant context) for cross-tenant wallet administration.","Clear cached request ids when switching tenant logins.","Keep the ownership check in the handler even if DbContext filters are added later."],"tags":["multitenancy","authorization","wallet","cross-tenant"],"backgroundTag":"permission-denied","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}