Skip to content

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

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

[PM-40514] Add custom attribute to check Organization abilities, use for risk insights endpoints #8240
lastbestdev wants to merge 11 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 and applies it to the Access Intelligence endpoints in OrganizationReportsController and ReportsController, while removing the inline UseRiskInsights check from OrganizationReportsController.AuthorizeAsync. Endpoints that take the organization ID from the request body keep a controller-level check, since the route-based attribute cannot resolve an org ID for them. No code commits have landed since the previous review round (only a merge from main), so the earlier open findings are re-stated below rather than duplicated as new inline comments.

Code Review Details

New finding this round

  • ♻️ : New attribute file lives in src/Api/Utilities but declares namespace Bit.Core.Utilities; it depends on Bit.Api.AdminConsole.Authorization so it cannot live in Core
    • src/Api/Utilities/RequireOrganizationAbilityAttribute.cs:8

Still open from earlier rounds (existing threads — not re-posted inline)

  • ❌ : Five actions in OrganizationReportsController still have no [RequireOrganizationAbility], and the inline UseRiskInsights check was removed from AuthorizeAsync in this PR. Net effect is that these five endpoints (two of them writes) lost their plan gate entirely rather than gaining one. Flagged by @AlexRubik; thread is now marked outdated but the code is unchanged.
    • src/Api/Dirt/Controllers/OrganizationReportsController.cs:299 (RenewFileUploadUrlAsync)
    • src/Api/Dirt/Controllers/OrganizationReportsController.cs:509 (GetOrganizationReportSummaryAsync)
    • src/Api/Dirt/Controllers/OrganizationReportsController.cs:527 (UpdateOrganizationReportSummaryAsync)
    • src/Api/Dirt/Controllers/OrganizationReportsController.cs:544 (GetOrganizationReportApplicationDataAsync)
    • src/Api/Dirt/Controllers/OrganizationReportsController.cs:563 (UpdateOrganizationReportApplicationDataAsync)
    • Related: the replacement comment at test/Api.Test/Dirt/OrganizationReportsControllerTests.cs:227 claims the attribute is on "each action", which is not currently accurate.
  • ⚠️ : Ability check runs before the AccessReports membership check, so a non-member gets 400 (org exists, no Risk Insights) vs 404 (org exists, has Risk Insights) — a cross-org plan oracle
    • src/Api/Utilities/RequireOrganizationAbilityAttribute.cs:47-59
  • ⚠️ : A caller-supplied empty GUID in the route throws a bare Exception, which ExceptionHandlerFilterAttribute maps to 500 rather than 400
    • src/Api/Utilities/RequireOrganizationAbilityAttribute.cs:42-45
  • 🎨 : Class summary uses // instead of /// so it never renders, references the wrong exception type (FeatureUnavailableException vs BadRequestException), and has a stray // </summary> at line 22
    • src/Api/Utilities/RequireOrganizationAbilityAttribute.cs:10-13, :22

Minor observations (no action required)

The same logical condition now returns 400 from the attribute path and 404 from ReportsController.AuthorizeAsync; note that the NotFoundException message at src/Api/Dirt/Controllers/ReportsController.cs:242 is discarded by ExceptionHandlerFilterAttribute, which always emits "Resource not found." for 404s. Replacing the .Result-blocking request.Any(...) check with an awaited loop in AddPasswordHealthReportApplications is a correctness improvement.

@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.22%. Comparing base (d5a350e) to head (f1e2c35).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8240      +/-   ##
==========================================
+ Coverage   63.21%   63.22%   +0.01%     
==========================================
  Files        2410     2411       +1     
  Lines      104450   104478      +28     
  Branches     9458     9462       +4     
==========================================
+ Hits        66026    66060      +34     
+ Misses      36165    36161       -4     
+ Partials     2259     2257       -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(

using Bit.Core.Models.Data.Organizations;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Bit.Core.Utilities;

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.

♻️ DEBT: New file lives in src/Api/Utilities but declares namespace Bit.Core.Utilities.

Details and fix

Every other file in src/Api/Utilities uses namespace Bit.Api.Utilities. This looks like a carry-over from src/Core/Utilities/RequireFeatureAttribute.cs, which this class is modeled on.

The type cannot actually live in Core — it depends on Bit.Api.AdminConsole.Authorization.HttpContextExtensions.GetOrganizationId() — so the namespace advertises availability from the Core assembly that does not exist, and Core-layer code that has using Bit.Core.Utilities; will not resolve it.

namespace Bit.Api.Utilities;

OrganizationReportsController already has using Bit.Api.Utilities;; ReportsController and test/Api.Test/Utilities/RequireOrganizationAbilityAttributeTests.cs would need the using swapped.

@lastbestdev

Copy link
Copy Markdown
Contributor Author

Closing this PR and reopening a fix without the custom attribute. Due to the complexity of trying to patch together existing authorization checks on Access Intelligence endpoints with the custom attribute, I'll defer creating it until another day.

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