{"record":{"id":"c060f383802830a0","repo":"bitwarden/server","slug":"unauthorized-c060f3","errorCode":null,"errorMessage":"Unauthorized.","messagePattern":"Unauthorized\\.","errorType":"http","errorClass":"UnauthorizedAccessException","httpStatus":401,"severity":"error","filePath":"src/Api/SecretsManager/Controllers/SecretsTrashController.cs","lineNumber":51,"sourceCode":"        _currentContext = currentContext;\n        _secretRepository = secretRepository;\n        _emptyTrashCommand = emptyTrashCommand;\n        _restoreTrashCommand = restoreTrashCommand;\n        _userService = userService;\n        _eventService = eventService;\n    }\n\n    [HttpGet(\"secrets/{organizationId}/trash\")]\n    public async Task<SecretWithProjectsListResponseModel> ListByOrganizationAsync(Guid organizationId)\n    {\n        if (!_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        if (!await _currentContext.OrganizationAdmin(organizationId))\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var secrets = await _secretRepository.GetManyDetailsByOrganizationIdInTrashAsync(organizationId);\n        return new SecretWithProjectsListResponseModel(secrets);\n    }\n\n    [HttpPost(\"secrets/{organizationId}/trash/empty\")]\n    public async Task EmptyTrashAsync(Guid organizationId, [FromBody] List<Guid> ids)\n    {\n        if (!_currentContext.AccessSecretsManager(organizationId))\n        {\n            throw new NotFoundException();\n        }\n\n        if (!await _currentContext.OrganizationAdmin(organizationId))\n        {\n            throw new UnauthorizedAccessException();\n        }","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/SecretsManager/Controllers/SecretsTrashController.cs#L33-L69","documentation":"Thrown at SecretsTrashController.cs:51 inside ListByOrganizationAsync. After the SM-access check passed, the second guard requires OrganizationAdmin; if false the controller throws UnauthorizedAccessException, which ExceptionHandlerEndpointFilter maps to HTTP 401 with body \"Unauthorized.\". Trash listing is admin-only: an SM member who is not an org admin cannot enumerate trashed secrets.","triggerScenarios":"GET /secrets/{orgId}/trash by a user who has Secrets Manager access but is not an organization admin (e.g. a custom role or standard SM user).","commonSituations":"Non-admin SM user opening the trash view; service account or limited role attempting trash enumeration; admin rights removed after SM seat granted.","solutions":["Have an organization admin perform the trash listing.","Promote the user to an admin role in the organization if trash access is required.","Use an admin-scoped API token for automated trash queries."],"exampleFix":"// before: SM user (non-admin) lists trash\nawait smUserClient.GetAsync($\"/secrets/{orgId}/trash\"); // 401\n\n// after: admin identity lists trash\nvar adminClient = ClientFor(orgAdminCredential);\nawait adminClient.GetAsync($\"/secrets/{orgId}/trash\");","handlingStrategy":"validation","validationCode":"if (!await IsOrganizationAdminAsync(orgId))\n    throw new UnauthorizedAccessException(\"Trash listing requires org admin\");\nawait adminClient.GetAsync($\"/secrets/{orgId}/trash\");","typeGuard":null,"tryCatchPattern":"try { await client.GetAsync($\"/secrets/{orgId}/trash\"); }\ncatch (ApiException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) { Log.Warn(\"Admin role required for trash\"); }","preventionTips":["Restrict the trash view to org admins.","Use an admin-scoped token for automated trash queries.","Re-check admin role when roles change."],"tags":["csharp","aspnet","bitwarden","secrets-manager","authorization","admin","trash","unauthorized","api"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}