{"record":{"id":"bd24195c5b9b9d8b","repo":"fullstackhero/dotnet-starter-kit","slug":"impersonation-grant-not-found","errorCode":null,"errorMessage":"impersonation grant not found","messagePattern":"impersonation grant not found","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/RevokeImpersonationGrant/RevokeImpersonationGrantCommandHandler.cs","lineNumber":40,"sourceCode":"        RevokeImpersonationGrantCommand request,\n        CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(request);\n\n        if (!currentUser.IsAuthenticated())\n        {\n            throw new UnauthorizedException();\n        }\n\n        var callerUserId = currentUser.GetUserId().ToString();\n        var callerTenantId = currentUser.GetTenant()\n            ?? throw new UnauthorizedException(\"missing tenant context\");\n        var isRoot = string.Equals(callerTenantId, MultitenancyConstants.Root.Id, StringComparison.Ordinal);\n\n        // Enforce visibility before revoking: tenant admins may only revoke grants in their own\n        // tenant. Cross-tenant grants return 404 (not 403) so existence isn't confirmed out of scope.\n        var grant = await grantService.GetByIdAsync(request.GrantId, cancellationToken).ConfigureAwait(false)\n            ?? throw new NotFoundException(\"impersonation grant not found\");\n\n        var withinTenant = string.Equals(grant.ImpersonatedTenantId, callerTenantId, StringComparison.Ordinal)\n            || string.Equals(grant.ActorTenantId, callerTenantId, StringComparison.Ordinal);\n\n        if (!isRoot && !withinTenant)\n        {\n            throw new NotFoundException(\"impersonation grant not found\");\n        }\n\n        var updated = await grantService.RevokeAsync(\n            id: request.GrantId,\n            revokedByUserId: callerUserId,\n            revokedByUserName: currentUser.Name,\n            reason: request.Reason,\n            ct: cancellationToken).ConfigureAwait(false);\n\n        // Surface revoke as a first-class security event, queryable alongside Start/End entries.\n        // The audit Reason is the revocation reason, not the original impersonation reason.","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Identity/Modules.Identity/Features/v1/Impersonation/RevokeImpersonationGrant/RevokeImpersonationGrantCommandHandler.cs#L22-L58","documentation":"The handler loads the impersonation grant by id via grantService.GetByIdAsync; when no grant with that id exists it throws NotFoundException('impersonation grant not found'), which maps to HTTP 404. This is the missing-entity case of the revoke flow.","triggerScenarios":"Passing a GrantId that does not exist (already revoked and hard-deleted, wrong id, or an id from another database) to the revoke-impersonation-grant endpoint.","commonSituations":"Client cached a grant id from a list that was since purged; id typo or GUID from a different environment (staging vs prod); grant revoked concurrently by another admin.","solutions":["Verify the grant id is current by re-fetching the impersonation grant list","Check you are calling the correct environment/database","Treat 404 as final for that id — do not retry"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"const grants = await api.listImpersonationGrants();\nif (!grants.some(g => g.id === grantId)) throw new Error(`grant ${grantId} not found; refresh list`);","typeGuard":null,"tryCatchPattern":"try { await api.revokeImpersonationGrant(grantId); }\ncatch (e) { if (e.status === 404) { refreshGrantList(); return; } throw e; }","preventionTips":["Re-fetch the grant list before revoking instead of caching ids","Never reuse grant ids across environments","Treat 404 as terminal — do not retry the revoke"],"tags":["not-found","impersonation","rest"],"backgroundTag":"resource-not-found","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"}