[PM-40514] Add custom attribute to check Organization abilities, use for risk insights endpoints - #8240
[PM-40514] Add custom attribute to check Organization abilities, use for risk insights endpoints #8240lastbestdev wants to merge 10 commits into
Conversation
🤖 Bitwarden Claude Code ReviewOverall Assessment: REQUEST CHANGES This PR introduces Code Review Details
Also noted, not commented inline: the file lives in |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8240 +/- ##
==========================================
+ Coverage 63.29% 63.31% +0.01%
==========================================
Files 2401 2402 +1
Lines 104043 104071 +28
Branches 9426 9430 +4
==========================================
+ Hits 65857 65891 +34
+ Misses 35930 35926 -4
+ Partials 2256 2254 -2 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| var orgAbilityCacheService = context.HttpContext.RequestServices.GetRequiredService<IOrganizationAbilityCacheService>(); | ||
|
|
||
| var orgAbility = await orgAbilityCacheService.GetOrganizationAbilityAsync(orgId); | ||
| if (orgAbility == null) | ||
| { | ||
| throw new BadRequestException("The user's organization does not have access to this feature in their plan."); | ||
| } | ||
|
|
||
| var hasAbility = (bool)_ability.GetValue(orgAbility)!; | ||
| if (!hasAbility) | ||
| { | ||
| throw new BadRequestException("The user's organization does not have access to this feature in their plan."); | ||
| } |
There was a problem hiding this comment.
Details and fix
[Authorize("Application")] only requires an authenticated user — it does not establish membership in {orgId}. Because this is an action filter, the ability lookup happens before the action body calls _currentContext.AccessReports(orgId).
For a caller who is not a member of the target org:
| Target org state | Before this PR | After this PR |
|---|---|---|
UseRiskInsights = true |
404 | 404 |
UseRiskInsights = false / not in cache |
404 | 400 "The user's organization does not have access to this feature in their plan." |
So any authenticated user holding an org GUID can now distinguish "org has Risk Insights" from "org does not / does not exist" on e.g. GET reports/organizations/{organizationId}/latest. Previously AuthorizeAsync ran AccessReports first and returned 404 uniformly.
Note that ReportsController.AuthorizeAsync (line 230) does this correctly — permission check first, and it throws NotFoundException for the ability failure — so the two halves of this feature now behave differently for the same condition.
Options:
- Have the attribute resolve org access (e.g.
ICurrentContext) before the ability lookup and throwNotFoundExceptionwhen the caller has no relationship to the org, or - Throw
NotFoundExceptionhere so the response is indistinguishable, matchingReportsController.AuthorizeAsync.
| if (orgId == Guid.Empty) | ||
| { | ||
| throw new Exception("Route parameter 'orgId' or 'organizationId' is missing or invalid."); | ||
| } |
There was a problem hiding this comment.
Details and fix
GetOrganizationId() parses 00000000-0000-0000-0000-000000000000 successfully, so GET reports/organizations/00000000-0000-0000-0000-000000000000/latest reaches this branch. A bare Exception falls into the else arm of ExceptionHandlerFilterAttribute, which logs LogError(0, exception, "Unhandled exception") and returns 500.
Before this PR, EnsureValidIds handled the same input with BadRequestException("OrganizationId is required.") → 400. This is client-triggerable, so it will also add noise to error monitoring.
if (orgId == Guid.Empty)
{
throw new BadRequestException("Route parameter 'orgId' or 'organizationId' is missing or invalid.");
}| // <summary> | ||
| /// Specifies that the class or method that this attribute is applied to requires the specified organization ability | ||
| /// to be enabled. If the organization ability is not enabled, a <see cref="FeatureUnavailableException"/> is thrown | ||
| // </summary> |
There was a problem hiding this comment.
🎨 SUGGESTED: The class summary uses // instead of ///, so it never renders, and it names the wrong exception.
Details and fix
Lines 10 and 13 open/close with //, which means the XML doc for the type is dropped entirely. The text also says FeatureUnavailableException (copied from RequireFeatureAttribute) while this attribute throws BadRequestException. There is also a stray // </summary> at line 22 inside the constructor docs.
/// <summary>
/// Specifies that the class or method that this attribute is applied to requires the specified organization ability
/// to be enabled. If the organization ability is not enabled, a <see cref="BadRequestException"/> is thrown.
/// </summary>Worth fixing on a new shared utility, since consumers will rely on the documented exception type.
| [RequireFeature(FeatureFlagKeys.AccessIntelligenceNewArchitecture)] | ||
| [HttpGet("{organizationId}/{reportId}/file/renew")] | ||
| public async Task<OrganizationReportFileResponseModel> RenewFileUploadUrlAsync( |
There was a problem hiding this comment.
Five actions never got the require org ability attribute: RenewFileUploadUrlAsync (line 298) plus the data/summary (508/526) and data/application (543/562) pairs, two of them writes. Each just needs [RequireOrganizationAbility(nameof(OrganizationAbility.UseRiskInsights))] above the method. Worth pairing it with a reflection test that fails if an endpoint here is missing the attribute, since the comment at OrganizationReportsControllerTests.cs:227 currently says it's on "each action".
[RequireFeature(FeatureFlagKeys.AccessIntelligenceNewArchitecture)]
[HttpGet("{organizationId}/{reportId}/file/renew")]
[RequireOrganizationAbility(nameof(OrganizationAbility.UseRiskInsights))] // + add this
public async Task<OrganizationReportFileResponseModel> RenewFileUploadUrlAsync(
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-40514
📔 Objective
Adds a reusable custom attribute for checking the presence of an Organization ability before allowing an API request to process and return a result.
The original ticket is for patching up access to v1 Access Intelligence endpoints (aka Risk Insights), 5 of which were not checking the newly added Organization ability for feature access. The new attribute is applied to all Access Intelligence endpoints to gate access.
📸 Screenshots
N/A