Skip to content

[PM-40514] Add custom attribute to check Organization abilities, use for risk insights endpoints - #8240

Open
lastbestdev wants to merge 10 commits into
mainfrom
vuln-685/dirt/access-intelligence-v1-endpoint-authz
Open

[PM-40514] Add custom attribute to check Organization abilities, use for risk insights endpoints #8240
lastbestdev wants to merge 10 commits into
mainfrom
vuln-685/dirt/access-intelligence-v1-endpoint-authz

Conversation

@lastbestdev

@lastbestdev lastbestdev commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🎟️ 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

@lastbestdev
lastbestdev requested a review from a team as a code owner August 20, 2026 23:50
@lastbestdev
lastbestdev requested a review from AlexRubik August 20, 2026 23:50
@github-actions

github-actions Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: REQUEST CHANGES

This PR introduces RequireOrganizationAbilityAttribute, an IAsyncActionFilter that resolves an OrganizationAbility boolean property by name and gates the action on it, then applies it to the Access Intelligence endpoints in OrganizationReportsController and ReportsController. The three endpoints that take the organization ID from the request body keep an in-controller AuthorizeAsync helper instead, and the previously duplicated ability check was removed from OrganizationReportsController.AuthorizeAsync. Unit tests cover the new attribute (route parameter resolution, ability resolution by name, constructor validation) and the body-based endpoints. The main concern is ordering: the filter runs before the per-endpoint AccessReports permission check, which changes what a non-member sees.

Code Review Details
  • ⚠️ : Ability check runs before the membership check, letting any authenticated user distinguish orgs with Risk Insights (400) from those without (404) by org GUID
    • src/Api/Utilities/RequireOrganizationAbilityAttribute.cs:47-59
  • ⚠️ : An all-zeros GUID in the route throws a bare Exception and returns 500 where the previous EnsureValidIds path returned 400
    • src/Api/Utilities/RequireOrganizationAbilityAttribute.cs:42-45
  • 🎨 : Class XML summary uses // so it never renders, and documents FeatureUnavailableException while the attribute throws BadRequestException
    • src/Api/Utilities/RequireOrganizationAbilityAttribute.cs:10-13

Also noted, not commented inline: the file lives in src/Api/Utilities/ but declares namespace Bit.Core.Utilities, and the NotFoundException message in ReportsController.AuthorizeAsync is discarded by ExceptionHandlerFilterAttribute in favour of "Resource not found."

@lastbestdev lastbestdev added the t:bugfix Change Type - Bugfix label Aug 20, 2026
Comment thread src/Api/Dirt/Controllers/ReportsController.cs Outdated
@lastbestdev lastbestdev changed the title [VULN-685] Add custom attribute to check Organization abilities, use for risk insights endpoints [PM-40514] Add custom attribute to check Organization abilities, use for risk insights endpoints Aug 21, 2026
@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.31%. Comparing base (ac309aa) to head (6796de1).
⚠️ Report is 2 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@AlexRubik AlexRubik added the ai-review Request a Claude code review label Aug 24, 2026
Comment on lines +47 to +59
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.");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The ability check now runs before the membership check, turning these endpoints into a cross-org plan oracle.

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 throw NotFoundException when the caller has no relationship to the org, or
  • Throw NotFoundException here so the response is indistinguishable, matching ReportsController.AuthorizeAsync.

Comment on lines +42 to +45
if (orgId == Guid.Empty)
{
throw new Exception("Route parameter 'orgId' or 'organizationId' is missing or invalid.");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: A caller-supplied empty GUID in the route now produces a 500 instead of a 400.

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.");
}

Comment on lines +10 to +13
// <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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎨 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.

Comment on lines 297 to 299
[RequireFeature(FeatureFlagKeys.AccessIntelligenceNewArchitecture)]
[HttpGet("{organizationId}/{reportId}/file/renew")]
public async Task<OrganizationReportFileResponseModel> RenewFileUploadUrlAsync(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Request a Claude code review t:bugfix Change Type - Bugfix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants