Skip to content

[PM-40336] Access Rules: API - #7983

Open
Hinton wants to merge 9 commits into
pam/access-rulefrom
pam/access-rule-api
Open

[PM-40336] Access Rules: API#7983
Hinton wants to merge 9 commits into
pam/access-rulefrom
pam/access-rule-api

Conversation

@Hinton

@Hinton Hinton commented Jul 14, 2026

Copy link
Copy Markdown
Member

🎟️ Tracking

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

📔 Objective

Add the commercial CRUD API for org-scoped PAM Access Rules, consuming the
domain and persistence layer from #7981. This PR is stacked on #7981 — review
and merge that first; GitHub will retarget this PR to main once it lands.

All endpoints are gated behind the pm-37044-pam-v-0 (FeatureFlagKeys.Pam)
feature flag, so nothing is reachable in production until PAM v0 ships. Access-rule
data (name, description, conditions) is organization configuration metadata, not
Vault Data — no zero-knowledge surface is touched.

What's included

API — bitwarden_license/src/Services/Pam

  • CRUD endpoints on the AccessRule group: GET (list), GET {id}, POST,
    PUT {id}, DELETE {id}, all org-scoped and feature-flag gated.
  • Request/response models (AccessRuleRequestModel, AccessRuleResponseModel)
    and the condition models: human_approval and ip_allowlist.
  • Create/Update/Delete commands behind interfaces; AccessRuleValidator
    validating the conditions JSON (max 10 conditions, per-type rules; empty list is
    vacuously valid and still routes access through PAM for audit).
  • Command and validator unit tests.

📸 Screenshots

N/A — backend only.


Update: authorization now runs in the middleware

AccessRuleEndpointsHandler was hand-rolling ICurrentContext membership and admin
checks. Now that #8095 has landed on
main, Pam can reference src/Libraries/OrganizationAuthorization directly and
authorize the way controllers do — which is the follow-up that PR deliberately left
out ("Pam is not wired up here — that belongs with the endpoint changes that consume
it").

  • MemberOrProviderRequirement on the group, matching the OrganizationUser check it
    replaces.
  • A new ManageAccessRules custom permission, with ManageAccessRulesRequirement on
    POST/PUT/DELETE. ASP.NET combines the group and endpoint policies, so a write
    has to satisfy both.
  • The rule.OrganizationId != orgId check stays in the handler — that is resource
    scoping, not authorization.
  • /leases, /access-requests and /leases/ciphers/{id} are untouched: they carry no
    {orgId} in the route, so IOrganizationRequirement cannot bind.

Behaviour change: a non-member now gets 403 rather than the 404 the handler
used to throw. That matches every other organization-scoped endpoint in Api, and
these endpoints are unreleased and gated behind FeatureFlagKeys.Pam.

Important

@bitwarden/team-admin-console-dev — the first of the two new commits
(Add the ManageAccessRules custom permission) is AC-owned and PAM-free: it touches
Permissions, Claims, CurrentContext, OrganizationService, the
OrganizationAuthorization library, and the public API PermissionsModel. Happy to
split it into its own PR against main if you would rather review it separately.

Two parts of it are load-bearing rather than mechanical:

  • The guard in OrganizationService.ValidateCustomPermissionsGrant — that path is an
    explicit per-permission if-chain, and without an entry a Custom member holding
    ManageUsers could grant a permission they do not hold themselves. The newer
    OrganizationUserValidationService walks ClaimsMap generically and needs no change.
  • PermissionsModel carries the field in both directions, because the public API
    round-trips a member through it — omitting it would silently strip an existing grant
    on update.

No migration: Permissions is a JSON blob on OrganizationUser. Granting the permission
from the web vault still needs a matching toggle in bitwarden/clients; until that ships
the permission is never set, so Owners, Admins and providers retain exactly today's access.

@github-actions

github-actions Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the commercial CRUD API for org-scoped PAM Access Rules — the endpoint handler, request/response models, condition models, create/update/delete commands, and the shared write validator, plus their unit tests. Security-sensitive paths are sound: every endpoint is gated behind Policies.Application, the FeatureFlagKeys.Pam feature flag, and per-request org membership/admin checks, with rules scoped to orgId and 404s used to avoid leaking existence. Input validation (conditions JSON shape, condition count cap, CIDR parsing, name uniqueness, collection ownership/conflict) is thorough and well covered by the command and validator tests. Access-rule data is organization configuration metadata, not Vault Data, so no zero-knowledge surface is affected.

Code Review Details

No blocking findings.

Notes considered and not flagged:

  • The handler's EnsureMemberAsync/EnsureAdminAsync authorization is already discussed in an open thread where the author proposes lifting it into RequireAuthorization; not duplicating here.
  • DI registration uses AddScoped/AddSingleton rather than TryAdd*, matching the existing convention in ServiceCollectionExtensions.
  • packages.lock.json changes reflect the internal Pam.Domain/Data project references added to Pam.csproj (transitive lock regeneration) — no new external dependency, no AppSec approval needed.

@Hinton
Hinton marked this pull request as draft July 14, 2026 17:36
@Hinton
Hinton force-pushed the pam/access-rule-api branch 2 times, most recently from 0351a28 to 37a5507 Compare July 14, 2026 18:06
@Hinton
Hinton force-pushed the pam/access-rule-api branch from 37a5507 to a85b172 Compare July 14, 2026 18:14
@Hinton
Hinton force-pushed the pam/access-rule-api branch from a85b172 to 9c09687 Compare July 14, 2026 18:16
@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.79310% with 105 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.80%. Comparing base (1ebb061) to head (c52178c).

Files with missing lines Patch % Lines
...Pam/Api/Models/Response/AccessRuleResponseModel.cs 0.00% 39 Missing ⚠️
...i/Endpoints/Handlers/AccessRuleEndpointsHandler.cs 0.00% 33 Missing ⚠️
...s/Pam/Api/Models/Request/AccessRuleRequestModel.cs 0.00% 18 Missing ⚠️
...e/src/Services/Pam/Services/AccessRuleValidator.cs 85.41% 4 Missing and 3 partials ⚠️
src/Core/Context/CurrentContext.cs 20.00% 4 Missing ⚠️
...s/Pam/Api/Models/Response/PamDateTimeExtensions.cs 0.00% 2 Missing ⚠️
.../Services/Pam/Services/AccessRuleWriteValidator.cs 95.65% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@                 Coverage Diff                 @@
##           pam/access-rule    #7983      +/-   ##
===================================================
- Coverage            67.30%   62.80%   -4.51%     
===================================================
  Files                 2307     2315       +8     
  Lines               100433   100700     +267     
  Branches              9028     9052      +24     
===================================================
- Hits                 67599    63241    -4358     
- Misses               30553    35265    +4712     
+ Partials              2281     2194      -87     

☔ 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/access-rule-api branch 2 times, most recently from 56c6cac to 44b46da Compare July 16, 2026 10:04
Comment thread bitwarden_license/src/Services/Pam/Services/AccessRuleValidator.cs Dismissed
Comment thread src/Infrastructure.Dapper/Pam/Repositories/AccessRuleRepository.cs Dismissed
Comment thread src/Infrastructure.EntityFramework/Pam/Models/AccessRule.cs Dismissed
@Hinton
Hinton force-pushed the pam/access-rule-api branch 2 times, most recently from c17c469 to ccf6444 Compare July 17, 2026 13:45
@Hinton Hinton added the t:feature Change Type - Feature Development label Jul 27, 2026
@Hinton
Hinton marked this pull request as ready for review July 27, 2026 12:08
@Hinton
Hinton force-pushed the pam/access-rule-api branch from e5f25d5 to 801a80f Compare July 27, 2026 13:38
@Hinton
Hinton requested review from a team as code owners July 27, 2026 15:22
@Hinton
Hinton requested a review from BTreston July 27, 2026 15:22
@Hinton
Hinton force-pushed the pam/access-rule-api branch from 801a80f to ca963d2 Compare July 27, 2026 15:22

@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 was requested for review but it doesn't look like it's needed anymore. Let me know if I've misunderstood.

Agreed that we'll extract the authorization logic in #8095.

@Hinton
Hinton force-pushed the pam/access-rule-api branch from d6deeaa to e13bb46 Compare July 31, 2026 15:03
@Hinton
Hinton requested a review from a team as a code owner August 3, 2026 08:45
@Hinton
Hinton requested a review from ike-kottlowski August 3, 2026 08:45
@Hinton
Hinton force-pushed the pam/access-rule-api branch from e13bb46 to e2e3279 Compare August 3, 2026 08:45
@Hinton
Hinton force-pushed the pam/access-rule-api branch from e2e3279 to d66c669 Compare August 3, 2026 08:59
Comment thread bitwarden_license/src/Services/Pam/Api/Endpoints/AccessRuleEndpoints.cs Outdated
@Hinton
Hinton force-pushed the pam/access-rule-api branch from d66c669 to c52178c Compare August 3, 2026 09:15
sutProvider.GetDependency<ICurrentContext>().ManageAccessRules(organization.Id).Returns(true);

await sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(
organization.Id, organizationUserInvite.Type.Value, null, invitePermissions);

var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.ValidateOrganizationUserUpdatePermissions(
organization.Id, organizationUserInvite.Type.Value, null, invitePermissions));
@Hinton
Hinton force-pushed the pam/access-rule-api branch from c52178c to 394ed90 Compare August 4, 2026 19:05
Hinton added 9 commits August 6, 2026 20:52
Remove the time_of_day condition kind along with the AccessWeekday enum and
its JSON converter, which existed only to type the weekday tokens inside a
time window. Access rules now expose human_approval and ip_allowlist; a
document that still carries a time_of_day entry is rejected as an unknown
kind.
The create and update commands each carried their own copy of the same
checks: name required, a positive maximum when extensions are allowed, the
conditions document, name uniqueness within the organization, and the
collection lookup that confirms every requested collection exists, belongs
to the organization, and is not already governed by another rule.

Move all of it into AccessRuleWriteValidator, which takes the id of the rule
being updated, or null when creating. Both places where the two paths differ
reduce to a comparison against that id: an update excludes itself from the
uniqueness check and may keep the collections it already governs, and with a
null id the same expressions give a create its stricter behaviour, where any
governed collection conflicts. The validator returns the deduplicated
collection ids so callers do not normalize them a second time.

The commands keep only what is theirs. Neither needs ICollectionRepository
any more, and update keeps its existence guard and the plain AccessRule it
maps for persistence.

Update now resolves the rule before judging the payload, so editing a rule
that does not exist, or belongs to another organization, is a 404 rather
than a 400 from a field check. The command tests assert the validator is
never reached in those cases to hold that order in place, and the shared
rules are covered once in AccessRuleWriteValidatorTests, including the
create and update asymmetries.
….csproj

Pam.csproj now references Pam.Domain directly for the AccessRule command
and validator implementations, so consumers of the Pam service need the
reference recorded in their own lock files too. Force-evaluated a
full-solution restore to bring every lock file back in sync.
The access-rule commands now take ICollectionRepository for the collection
association write, matching the method's new home after review feedback on
#7981.
PAM's access-rule endpoints need an organization-scoped permission to authorize
against. Adding it to Permissions rather than inventing a PAM-local requirement
means Custom members can be granted rule administration without also being made
Admins, and it reuses BasePermissionRequirement so Owners, Admins and providers
are covered by the same rule everything else follows.

Permissions is a JSON blob on OrganizationUser, so there is no migration.

The guard in OrganizationService.ValidateCustomPermissionsGrant is load-bearing
rather than mechanical: that path is an explicit per-permission if-chain, and
without an entry a Custom member holding ManageUsers could grant a permission
they do not hold themselves. The newer OrganizationUserValidationService walks
ClaimsMap generically and needs no change.

PermissionsModel carries the new field in both directions because the public
API round-trips a member through it — omitting it would silently strip an
existing grant on update.

Granting this from the web vault still needs a matching toggle in clients.
Until that ships the permission is simply never set, so behaviour is unchanged.
The handler was hand-rolling ICurrentContext membership and admin checks, which
is what OrganizationAuthorization was extracted out of Api to avoid. Pam can
reference the library directly now, so the endpoints authorize the way
controllers do: MemberRequirement on the group and ManageAccessRulesRequirement
on the write endpoints. ASP.NET combines the group and endpoint policies, so a
write has to satisfy both.

The group uses MemberRequirement rather than MemberOrProviderRequirement, which
narrows what the handler used to allow: currentContext.OrganizationUser() falls
through to OrganizationOwner(), which authorizes a provider for the
organization. Providers manage an organization's billing and configuration, but
access rules gate who can lease credentials out of it, and that is not theirs to
change.

Worth knowing for later: the group gate is the only thing keeping providers out.
ManageAccessRulesRequirement derives from BasePermissionRequirement, whose final
case authorizes a provider for the organization, so widening the group would
silently readmit them to the writes as well. There is a test for that.

No DI change — Api already calls AddOrganizationAuthorization() from
AddAdminConsoleAuthorizationHandlers, and Pam is hosted in Api.

Behaviour change: an unauthorized caller now gets 403 rather than the 404 the
handler used to throw. That matches every other organization-scoped endpoint in
Api, and these endpoints are unreleased and gated behind FeatureFlagKeys.Pam.

The rule.OrganizationId != orgId check stays in the handler. That one is
resource scoping rather than authorization: it stops a rule ID from one
organization being read through another organization's route.

Leaves /leases, /access-requests and /leases/ciphers/{id} alone — they carry no
{orgId} in the route, so IOrganizationRequirement cannot bind, and their
handlers are still NotImplementedException scaffolds.

Adds tests over the materialized endpoint metadata, since nothing in the
handler fails closed any more if a RequireAuthorization call is dropped.
@Hinton
Hinton force-pushed the pam/access-rule-api branch from 394ed90 to 919ae0d Compare August 6, 2026 19:16
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.

2 participants