Skip to content

[PM-41272] Extract organization authorization into a new library - #8095

Open
Hinton wants to merge 3 commits into
mainfrom
ac/extract-organization-authorization
Open

[PM-41272] Extract organization authorization into a new library#8095
Hinton wants to merge 3 commits into
mainfrom
ac/extract-organization-authorization

Conversation

@Hinton

@Hinton Hinton commented Jul 30, 2026

Copy link
Copy Markdown
Member

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-41272

📔 Objective

Move the generic organization/provider requirement plumbing out of src/Api/AdminConsole/Authorization and into a new src/Libraries/OrganizationAuthorization, so any host can authorize organization-scoped endpoints through the authorization middleware instead of hand-rolling ICurrentContext checks inside handlers.

What moved

  • IOrganizationRequirement + OrganizationRequirementHandler, OrganizationContext, OrganizationClaimsExtensions, and the five organization requirements
  • The whole Providers/ tree — identical plumbing, identical dependencies, no reason to split it
  • AuthorizeAttribute<T> and HttpContextExtensions

What deliberately stayed in Api

Collections/*, the resource-based handlers (RecoverAccountAuthorizationHandler, OrgUserLinkedToUserIdHandler, OrganizationCollectionManagementAccessHandler), and NoopAuthorizeAttribute. Those authorize over specific Api domain models rather than over the organization or provider on the route, and have no non-Api consumers.

This is a pure move

Following the SsrfProtection and Data precedent, the extracted types keep their existing namespaces. None of the 173 call sites in Api needed touching.

Every moved file is byte-identical to its original except for three added Microsoft.AspNetCore.* usings, which Api was getting implicitly from the Web SDK:

File Added
HttpContextExtensions.cs Microsoft.AspNetCore.Http, Microsoft.AspNetCore.Routing
Organizations/OrganizationRequirementHandler.cs Microsoft.AspNetCore.Http
Providers/ProviderRequirementHandler.cs Microsoft.AspNetCore.Http

All 13 moved test files are pure moves with no edits at all. The only genuinely new code is OrganizationAuthorizationServiceCollectionExtensions (the library's AddFoo entry point per LIBRARY.md), the two .csproj files, and README.md.

Renaming the namespaces to Bit.OrganizationAuthorization is a follow-up we can take whenever it suits, independent of this move.

👀 One thing worth a look

The library sits above Core, not below it. LIBRARY.md wants new libraries below Core, so README.md records every borrowed Core type. Per discussion below, depending on Core is the accepted position for now — unwinding it would mean extracting the Admin Console data models, identity, and organizations, which is its own effort. The table is there so those pieces are known, not because they are queued up.

Comment thread .github/CODEOWNERS Outdated
Comment thread src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs Outdated
Comment thread src/Libraries/OrganizationAuthorization/README.md Outdated
@Hinton Hinton added the t:tech-debt Change Type - Tech debt label Jul 30, 2026
@Hinton

Hinton commented Jul 30, 2026

Copy link
Copy Markdown
Member Author

@eliykat This is just a quick extraction as we discussed briefly in DM. If you want to tweak something, feel free to go ahead and do it directly on this branch. I think since it only moves files we shouldn't need to do any QA testing prior to merge?

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 62.87%. Comparing base (b4d4c55) to head (51dae70).

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #8095       +/-   ##
===========================================
+ Coverage   14.99%   62.87%   +47.88%     
===========================================
  Files        1409     2302      +893     
  Lines       61097   100219    +39122     
  Branches     4864     9019     +4155     
===========================================
+ Hits         9161    63015    +53854     
+ Misses      51780    35020    -16760     
- Partials      156     2184     +2028     

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

eliykat
eliykat previously approved these changes Jul 31, 2026

@eliykat eliykat left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @Hinton !

I agree this is OK to merge without QA, maybe just not immediately before rc cut.

Comment on lines +36 to +40
```csharp
endpoints.MapGroup("/organizations/{orgId:guid}/access-rules")
.RequireAuthorization(policy => policy.AddRequirements(new ManageAccessRulesRequirement()))
.MapAccessRuleEndpoints();
```

@eliykat eliykat Jul 31, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Non-blocking but I'm interested in feedback (including @justindbaur): we currently expose these as authorization requirements rather than authorization policies, because (a) I dislike magic strings that policies use as identifiers, and (b) there is only 1 requirement per endpoint, whereas a policy represents a collection of requirements + authentication schemes.

However, it looks like RequireAuthorization only accepts policy strings, or policies, or a policy builder callback, all of which is a bit verbose. We could instead package up each requirement into its own authorization policy:

public static class OrganizationPolicy
{
    public static AuthorizationPolicy ManageAccessRules
        => new([new ManageAccessRulesRequirement()], []); // let the consumer dictate the authentication scheme

}

And then usage becomes:

endpoints.MapGroup("/organizations/{orgId:guid}/access-rules")
    .RequireAuthorization(OrganizationPolicy.ManageAccessRules)
    .MapAccessRuleEndpoints();

which I think is much nicer for consumers and seems to be what ASP.NET wants you to do. The static class also makes the policies easy to find.

PS. "policy" is unfortunate here because we also have a policy domain, but if you're importing from the OrganizationAuthorization library hopefully that context is clear.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

We could call it OrganizationAuthorizationPolicy?

@Hinton Hinton Jul 31, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It seems we can do:

  endpoints.MapGroup("/organizations/{orgId:guid}/access-rules")
      .RequireAuthorization(new AuthorizeAttribute<ManageAccessRulesRequirement>())
      .MapAccessRuleEndpoints();

Might be good enough?

@Hinton
Hinton marked this pull request as ready for review July 31, 2026 07:52
@Hinton
Hinton requested review from a team as code owners July 31, 2026 07:52
@Hinton
Hinton requested a review from sven-bitwarden July 31, 2026 07:52
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the extraction of organization/provider authorization plumbing from src/Api/AdminConsole/Authorization into the new src/Libraries/OrganizationAuthorization library. Verified with rename detection that all 22 source and test files are pure moves (only three added Microsoft.AspNetCore.* usings that Api previously inherited implicitly from the Web SDK), and that the new AddOrganizationAuthorization() registers exactly the two handlers and IOrganizationContext binding that AddAdminConsoleAuthorizationHandlers previously registered, using TryAdd* per ADR-0026. Confirmed every consumer of the moved namespaces lives in Api/Api.Test, which now pick the types up transitively via the new ProjectReference, and that solution, CODEOWNERS, and packages.lock.json entries are all consistent with the Data/SsrfProtection library precedent.

Code Review Details

No blocking findings.

Notes considered and deliberately not raised as findings:

  • The Bit.Api.AdminConsole.* namespaces retained inside the new library and the library sitting above Core are both explicitly documented as accepted follow-ups in the PR description and README.md, and were agreed in existing review threads.
  • The minimal-API RequireAuthorization ergonomics are already under active discussion in an open thread on README.md:36-40; no duplicate comment added.

Hinton added 3 commits July 31, 2026 16:11
…horization

The organization and provider requirement plumbing lived in src/Api, so hosts
that cannot reference Api could not use it. Pam is the immediate case: Api
already references Pam, so the dependency cannot go the other way, and its
endpoint handlers were left doing imperative ICurrentContext checks instead of
authorizing through the middleware.

Core is not an option here — these types read user claims and depend on how the
host authenticates, which is why IOrganizationContext carries an explicit "do
not move this into Core" note. A library is the sanctioned alternative, and
src/Libraries already has the shape (LIBRARY.md, ADR-0031, ADR-0032) plus two
precedents in SsrfProtection and HttpExtensions.

What moved:
- IOrganizationRequirement and its handler, OrganizationContext,
  OrganizationClaimsExtensions, and the five organization requirements
- The Providers tree — same plumbing, same dependencies
- AuthorizeAttribute<T> and HttpContextExtensions

What deliberately stayed in Api: Collections, the resource-based handlers that
authorize over specific Api domain models, and NoopAuthorizeAttribute (an Api
controller convention).

The namespaces are unchanged, following the SsrfProtection and Data precedent,
so none of the 173 call sites in Api needed touching.

Two things worth reviewing:
- The handlers and OrganizationContext no longer take IUserService just to call
  GetProperUserId. UserClaimsExtensions.GetUserId reads the sub claim instead,
  which is what AddCustomIdentityServices configures UserIdClaimType to and
  what CurrentContext already reads directly. Each call site's null behaviour is
  preserved. This is the only behavioural change.
- The library sits above Core rather than below it. README.md records every
  borrowed Core type as debt, per LIBRARY.md.

Pam is not wired up here — that belongs with the endpoint changes that consume
it, so this stays a pure extraction that can merge on its own.
- Keep IUserService rather than reading the sub claim directly. It's in Core,
  which this library already depends on, so there's nothing to gain by avoiding
  it. Every moved file is now byte-identical to its original apart from the
  Microsoft.AspNetCore.* usings that Api got implicitly from the Web SDK.
- Trim the DI comment down to what's relevant to Api.
- Drop the trailing explanation from the CODEOWNERS entry.
- Record IUserService in the README's Core debt table, and note that depending
  on Core is the accepted position for now.
Reuses the same requirement-binding mechanism controllers already use
via [Authorize<T>], instead of a policy-builder callback, avoiding a
named policy. Add a TestHost-backed regression test proving the
combined requirement is enforced end-to-end.
@Hinton
Hinton force-pushed the ac/extract-organization-authorization branch from b52b8b7 to 51dae70 Compare July 31, 2026 14:21
@Hinton
Hinton requested a review from justindbaur July 31, 2026 14:24
@Hinton Hinton changed the title Extract organization authorization into src/Libraries/OrganizationAuthorization [PM-41272] Extract organization authorization into a new library Jul 31, 2026

@eliykat eliykat left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also a good solution, thanks!

}
}

private static async Task<IHost> BuildHostAsync() =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice tests!

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

Labels

t:tech-debt Change Type - Tech debt

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants