Skip to content

[PM-40211] Authorize Access Rule endpoints with IOrganizationRequirement - #8162

Open
Hinton wants to merge 3 commits into
pam/enable-pam-flowsfrom
pam/authorize-access-rules
Open

[PM-40211] Authorize Access Rule endpoints with IOrganizationRequirement#8162
Hinton wants to merge 3 commits into
pam/enable-pam-flowsfrom
pam/authorize-access-rules

Conversation

@Hinton

@Hinton Hinton commented Aug 7, 2026

Copy link
Copy Markdown
Member

🎟️ Tracking

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

📔 Objective

Declarative authorization on the access-rule routes (ADR-0022): reads require organization membership, writes require authority over rule authorship via the new ManageAccessRulesRequirement.

Worth flagging for review:

  • Deviation: no named-policy bridge. The story specifies policy-name constants in Policies.cs registered in Api/Startup.cs, because Pam.csproj cannot reference Api and so could not reach AuthorizeAttribute<T>. PM-41272 has since extracted that code into src/Libraries/OrganizationAuthorization, which depends only on Core — so Pam references the library directly and attaches requirements per route. Policies.cs and Startup.cs are untouched.
  • Requirements combine with, not replace, the group policy. They're carried as endpoint metadata, which AuthorizationMiddleware combines with the group-level Policies.Application; a test asserts both are present on every route. OrganizationRequirementHandler resolves the org from the {orgId:guid} group prefix and is already DI-registered.
  • Enforcement is covered end to end. AccessRuleAuthorizationTests (Api.IntegrationTest) drives the real pipeline: non-members, plain members, Custom users without ManageAccessRules, and provider users all get 403; members and Owners get through. Presence in metadata is not enforcement, so the registration tests alone would not catch an AllowAnonymous, a group policy that replaced rather than combined, or a new route added without the attribute. Only denials are asserted, so these stay valid once the handler scaffolds are implemented.
  • Scope: the Access Rule half only. The usage gate on access-request submission (which must check the member's AccessPam and the org's UsePam) and the CipherLeaseGate parity decision cannot land yet — SubmitAccessRequestCommand does not exist on main, since the access-request and cipher-lease handlers are still NotImplementedException scaffolds. There is no submission path to gate; that work belongs with the request/lease behaviour slices. For the same reason there were no imperative EnsureMemberAsync/EnsureAdminAsync checks to delete — AccessRuleEndpointsHandler never had them on main. PM-40211 should stay open for the remaining half.
  • Only one of the two observable changes flagged in breakdown review note 8 applies here. Unauthorized calls return 403 from the middleware rather than 404 from a handler. The other — that BasePermissionRequirement/MemberOrProviderRequirement authorize provider users — is deliberately avoided: the group uses MemberRequirement, and ManageAccessRulesRequirement implements IOrganizationRequirement directly rather than deriving from BasePermissionRequirement. Providers are denied on both reads and writes — access rules gate who can lease credentials out of an organization, which is not theirs to read or change. Two tests pin this, including MapPamEndpoints_AccessRulesNeverAuthorizeProvidersByMembership.

Lockfile changes are the new project reference only — no platform churn.

Depends on #8160 and #8161 — review those first.

@Hinton
Hinton force-pushed the pam/authorize-access-rules branch from a3b75df to ae5a67e Compare August 7, 2026 11:11
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 63.55%. Comparing base (31ab8a6) to head (85c150d).

Additional details and impacted files
@@                   Coverage Diff                    @@
##           pam/enable-pam-flows    #8162      +/-   ##
========================================================
- Coverage                 67.92%   63.55%   -4.38%     
========================================================
  Files                      2338     2339       +1     
  Lines                    101555   101566      +11     
  Branches                   9186     9187       +1     
========================================================
- Hits                      68983    64547    -4436     
- Misses                    30265    34805    +4540     
+ Partials                   2307     2214      -93     

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

@Hinton
Hinton force-pushed the pam/authorize-access-rules branch from ae5a67e to f10979c Compare August 7, 2026 11:36
@Hinton
Hinton force-pushed the pam/authorize-access-rules branch from f10979c to 0356bd4 Compare August 7, 2026 12:00
@Hinton Hinton added the t:feature Change Type - Feature Development label Aug 7, 2026
@Hinton
Hinton marked this pull request as ready for review August 7, 2026 12:02
@Hinton
Hinton requested review from a team as code owners August 7, 2026 12:02
@Hinton
Hinton requested a review from eliykat August 7, 2026 12:02
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the declarative authorization added to the PAM access-rule routes: a group-level MemberRequirement plus ManageAccessRulesRequirement on the three write endpoints, the new ManageAccessRulesRequirement in PermissionRequirements.cs, and the OrganizationAuthorization project reference from Pam.csproj. Verified that Permissions.ManageAccessRules and its claim already exist, that OrganizationRequirementHandler is DI-registered in Api via AddAuthorizationHandlers(), that the {orgId:guid} group prefix satisfies HttpContextExtensions.GetOrganizationId(), and that ASP.NET's AuthorizationPolicy.CombineAsync accumulates the group's Policies.Application with the per-endpoint requirement metadata rather than replacing it — matching what the new tests assert. Lock files are consistent with the single project-reference addition across all six affected manifests; no new NuGet packages, so no AppSec dependency approval is required.

Code Review Details

No blocking findings.

Notes considered and intentionally not raised as findings:

  • Reads being gated at plain organization membership is stated as the intended design in the PR description, and the handlers are still NotImplementedException scaffolds, so there is no live data exposure.
  • A provider user who is also an org member would pass both gates on writes; this is inherent to BasePermissionRequirement's provider fallback across all existing consumers, not introduced here.
  • The group documents 400/404 in OpenAPI but not 401/403; a documentation-only gap.

/// rules gate who can lease credentials out of it, which is not theirs to change. Note this group gate is the only
/// thing keeping providers out — <see cref="ManageAccessRulesRequirement"/> derives from
/// <c>BasePermissionRequirement</c>, which falls back to authorizing a provider for the organization. Removing or
/// weakening the group requirement would silently readmit them to the write endpoints.

@eliykat eliykat Aug 7, 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.

This suggests that you don't actually want to reuse BasePermissionRequirement because you don't want to authorize providers. It works for most of our custom permissions, but you can always implement IOrganizationRequirement directly if you don't want that behavior.

For example:

public class ManageAccessRulesRequirement : IOrganizationRequirement
{
    public Task<bool> AuthorizeAsync(CurrentContextOrganization? organizationClaims,
        Func<Task<bool>> isProviderUserForOrg)
    {
        var authorized = organizationClaims is
            { Type: OrganizationUserType.Owner }
            or { Type: OrganizationUserType.Admin }
            or { Type: OrganizationUserType.Custom, Permissions.ManageAccessRules : true };

        return Task.FromResult(authorized);
    }
}

This can be used inside Authorize<T> in the same way, so no change to the consumers.

You can own this requirement if desired, because it reflects the authorization rules for your domain.

patriksvensson
patriksvensson previously approved these changes Aug 10, 2026
@Hinton
Hinton dismissed patriksvensson’s stale review August 10, 2026 18:10

The merge-base changed after approval.

@Hinton
Hinton requested a review from a team as a code owner August 10, 2026 18:10
@Hinton
Hinton requested a review from enmande August 10, 2026 18:10
Hinton added 2 commits August 10, 2026 20:19
Declarative authorization on the access-rule routes (ADR-0022): the group requires
organization membership, and writes additionally require authority over rule
authorship via the new ManageAccessRulesRequirement. ASP.NET combines the group and
endpoint policies, so a write has to satisfy both.

Deviation from the story: no named-policy bridge. The story specifies policy-name
constants in Policies.cs registered in Api/Startup.cs, because Pam.csproj cannot
reference Api and so could not reach AuthorizeAttribute<T>. PM-41272 has since
extracted that authorization code into src/Libraries/OrganizationAuthorization,
which depends only on Core — so Pam references the library directly and attaches
AuthorizeAttribute<T> to the group and to the write routes. Policies.cs and
Startup.cs are untouched.

The requirements are carried as endpoint metadata, which AuthorizationMiddleware
combines with the group-level Policies.Application rather than replacing it; a test
asserts both are present on every route. OrganizationRequirementHandler resolves the
organization from the {orgId:guid} group prefix and is already DI-registered by
AddOrganizationAuthorization.

Providers are excluded from the resource entirely. The group gate is deliberately
MemberRequirement and not MemberOrProviderRequirement: providers manage an
organization's billing and configuration, but access rules gate who can lease
credentials out of it, which is not theirs to read or change. That gate is
load-bearing rather than decorative — ManageAccessRulesRequirement derives from
BasePermissionRequirement, whose final arm authorizes any provider for the
organization, so the write routes would admit providers on the permission alone.
Two tests pin this: every write carries MemberRequirement alongside the permission,
and no access-rule route carries MemberOrProviderRequirement.

Scope: this covers the Access Rule half of the story only. The remaining half — the
usage gate on access-request submission, which must check both the member's
AccessPam and the organization's UsePam, plus the CipherLeaseGate parity decision —
cannot land yet. SubmitAccessRequestCommand does not exist on main: the access-request
and cipher-lease handlers are still NotImplementedException scaffolds, so there is no
submission path to gate. That gate belongs with the request/lease behaviour slices.
For the same reason there are no imperative EnsureMemberAsync/EnsureAdminAsync checks
to delete — AccessRuleEndpointsHandler never had them on main.

Note the observable change the story flags (breakdown review note 8) holds here:
unauthorized calls now return 403 from the authorization middleware.
BasePermissionRequirement's final arm authorizes any provider managing the
organization, which is right for most custom permissions but not for access
rules: they gate who can lease credentials out of an organization, which is
not a provider's to change. Deriving from it left the group's MemberRequirement
as the only thing keeping providers off the write endpoints — coupling the
previous remarks had to document rather than remove.

Implement IOrganizationRequirement directly instead, and move the requirement
into the PAM project, which owns the domain rule. This reverts the additions to
the Admin Console's shared PermissionRequirements, so the PR no longer changes
any AC-owned file.

Also regenerates util/SqlServerEFScaffold's lock file, which transitively
references Pam and was missed when Pam.csproj gained OrganizationAuthorization.
@Hinton
Hinton force-pushed the pam/authorize-access-rules branch from 6afb818 to 40283ea Compare August 10, 2026 18:20
@Hinton
Hinton removed request for a team and enmande August 10, 2026 18:20
The endpoint-registration tests assert which requirements are attached to
which route, but stop before the pipeline runs — presence in metadata is
not enforcement. These cover the denials, which stay valid once the
handler scaffolds are implemented.
@Hinton
Hinton requested a review from patriksvensson August 10, 2026 18:49

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

AC no longer owns any files in this PR, so no approval required, but the authorization implementation looks good to me.

On more of a business logic / product note:

providers manage an organization's billing and configuration, but access rules gate who can lease credentials out of it, which is not theirs to read or change.

This might be overstating it a little - today providers can assign and remove access to items by managing collection assignments. They can't access items themselves, but it's well within their purpose to set up group and collection hierarchies for their clients to make sure they're using Bitwarden properly. I can imagine a use case where providers configure PAM rules for their clients as part of setting up their organization. It's OK if they're out of scope for the MVP, just wanted to clarify.

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

Labels

t:feature Change Type - Feature Development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants