Skip to content

[PM-35785] Strongly typed policy enforcement - #1364

Open
eliykat wants to merge 13 commits into
mainfrom
policy-data-v4
Open

[PM-35785] Strongly typed policy enforcement#1364
eliykat wants to merge 13 commits into
mainfrom
policy-data-v4

Conversation

@eliykat

@eliykat eliykat commented Aug 8, 2026

Copy link
Copy Markdown
Member

🎟️ Tracking

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

📔 Objective

Iterate on the policies crate to provide a better policy enforcement interface. This is done in 2 ways:

  • represent the outcome of a policy enforcement decision through a dedicated return value, EnforcedPolicy, rather than simply filtering PolicyViews out of an array (which is what filter_by_type does today)
  • handle per-policy data deserialization (some policies carry additional configuration data)

This PR is fairly large, but the commits are ordered to try to separate interesting vs. uninteresting changes. The commits, in order:

  1. add comprehensive tests for the filter_by_type interface before refactoring - to ensure we don't cause a regression in existing consumers.
  2. delete the runtime builder pattern, which overcomplicated the design. Replace it with a simple match statement on the PolicyType enum, which looks up the corresponding Policy implementation. Add implementations for each policy we have today.
  3. add the new policy enforcement API - this is the new business logic and (I suggest) where most of the review should focus.
  4. test cleanup.
  5. (PR review feedback after this point)

See the expanded README for an overview of the crate and how to add and consume policies.

One design note: there are separate client interfaces for native rust vs. FFI consumers. This is to accommodate the per-policy data. As the native rust interface shows, we can resolve this at compile time (using generics) for consumers, and I want to keep that as the first-class interface rather than forcing everyone to use the enum and have redundant match statements everywhere.

The next step after this PR would be to introduce state so that consumers are not passing in the policyViews and organization contexts. The new interfaces are not intended to be used yet, as that will be a breaking change.

🚨 Breaking Changes

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

🔍 SDK Breaking Change Detection

SDK Version: policy-data-v4 (8f16df9)

⚠️ If breaking changes are detected, a corresponding pull request addressing them must be ready for merge in the affected client repository.

Client Status Details
typescript ❌ Breaking changes detected Compilation failed with new SDK version. A corresponding pull request addressing the breaking changes must be ready for merge in bitwarden/clients. - View Details
android ✅ No breaking changes detected Compilation passed with new SDK version - View Details

Breaking change detection uses the build of the SDK from this branch, including any incompatibities pre-existing on or merged into this branch. Check the workflow logs to confirm.
Results update as workflows complete.

@codecov

codecov Bot commented Aug 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.72812% with 158 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.15%. Comparing base (99ffb6e) to head (0f63746).

Files with missing lines Patch % Lines
crates/bitwarden-policies/src/policies.rs 20.00% 132 Missing ⚠️
crates/bitwarden-policies/src/policy_type.rs 26.92% 19 Missing ⚠️
crates/bitwarden-policies/src/models.rs 94.64% 3 Missing ⚠️
crates/bitwarden-policies/src/policy.rs 99.08% 3 Missing ⚠️
crates/bitwarden-policies/src/policy_client.rs 99.74% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1364      +/-   ##
==========================================
- Coverage   86.28%   86.15%   -0.13%     
==========================================
  Files         500      501       +1     
  Lines       73417    73852     +435     
==========================================
+ Hits        63349    63629     +280     
- Misses      10068    10223     +155     

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

Remove PolicyRegistry and its builder in favor of PolicyType::resolve_policy,
a match over all variants. Add concrete Policy impls for every policy type and
move the filtering trait into policy.rs. Behavior-preserving.
Introduce the typed enforcement model (EnforcedPolicy, EnforcedPolicyErased,
ResolvedPolicyView, PolicyDataType) and expose get_enforced / get_many_enforced
plus their FFI-erased variants on PolicyClient and the uniffi client. Reimplement
filter_by_type on top of the enforced path.
@eliykat
eliykat marked this pull request as ready for review August 12, 2026 06:02
@eliykat
eliykat requested review from a team as code owners August 12, 2026 06:02
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

🤖 Bitwarden Claude Code Review

Overall Assessment: APPROVE

Reviewed the bitwarden-policies refactor: the runtime PolicyRegistry builder is replaced by an exhaustive PolicyType::resolve_policy match, and a new strongly-typed enforcement API (Policy::Data, EnforcedPolicy<P>, EnforcedPolicyErased/PolicyDataType) sits alongside the existing filter_by_type. I verified that every per-policy exempt_roles override from the deleted policy_overrides.rs is preserved in policies.rs (MasterPassword, PasswordGenerator, FreeFamiliesSponsorship, RemoveUnlockWithPin, RestrictedItemTypes, AutomaticUserConfirmation, OrganizationUserNotification, FillAssist → no exemptions; MaximumVaultTimeout → Owner only; all others → the Owner/Admin default), so filter_by_type keeps its previous behavior, and that no other crate consumed the removed policy_overrides/filter exports. Enforcement remains fail-closed for a missing org context while still respecting the policy's enabled flag, data parsing falls back to Default without panicking, and the FFI round trip is covered by the new integration tests.

Code Review Details

No findings at or above the reporting threshold.

All findings from the earlier review passes are confirmed addressed on this head: the disabled-policy/missing-context gate in ResolvedPolicyView::into_enforced, the get_all_enforced backfill for organizations whose only policy is of another type, the Data = () parse log downgraded to debug! with an accurate message, and the stale TODO plus outdated test names.

Dependency Changes

Package Change Ecosystem
serde_json New to this crate Cargo
tracing New to this crate Cargo

Both are existing workspace dependencies added via workspace = true, not new to the repository, and Cargo.lock matches the manifest change.

Comment thread crates/bitwarden-policies/src/models.rs Outdated
Comment thread crates/bitwarden-policies/src/policy.rs
Comment thread crates/bitwarden-policies/src/policy.rs

@djsmith85 djsmith85 left a comment

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.

Platform-owned changes are limited to the added integration tests.

Please extend the CODEOWNERS file with this line
crates/bitwarden-wasm-internal/integration-tests/tests/policies/** @bitwarden/team-admin-console-dev

@eliykat
eliykat requested a review from a team as a code owner August 12, 2026 22:18
@eliykat eliykat added the ai-review Request a Claude code review label Aug 12, 2026
Comment thread crates/bitwarden-policies/src/models.rs
Comment thread crates/bitwarden-policies/src/policy_client.rs

@djsmith85 djsmith85 left a comment

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.

Approving for platform, as there's no longer any changes platform owns.

@sven-bitwarden Please ensure to also review crates/bitwarden-wasm-internal/integration-tests/tests/policies/policy-client.test.ts as the CODEOWNERS changes are only enforced after this has merged.

@quexten quexten left a comment

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.

One design note: there are separate client interfaces for native rust vs. FFI consumers. This is to accommodate the per-policy data. As the native rust interface shows, we can resolve this at compile time (using generics) for consumers, and I want to keep that as the first-class interface rather than forcing everyone to use the enum and have redundant match statements everywhere.

IMO I'm not sure whether that's worth the complexity of maintaining two different APIs. It's easy enough to just match with a default catch arm. In rust this should be expected / reasonable to work with.

Anyways, interested to see how this feels when it is more widely used by other consumers, maybe we'll double down on it, or revise.

};
}

describe("PolicyClient", () => {

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.

Nice! Testing the FFI boundary!

///
/// Untyped FFI path: native/WASM callers pass a runtime `policy_type` integer.
/// Delegates to the registry, falling back to default rules for unknown types.
/// Not yet intended for consumer use: exposed across the FFI boundary for

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.

I wonder whether we should have a separate feature we compile the wasm build with that exposes these (and other test only) methods, but that is not enabled in prod builds.

///
/// Defaults to [`Owner`](OrganizationUserType::Owner) and
/// [`Admin`](OrganizationUserType::Admin).
fn exempt_roles(&self) -> &[OrganizationUserType] {

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.

I would argue that having a default makes this harder to reason about. It's a general problem with OOP in TS where subclasses make it very hard to reason about the full behavior. We usually do not have that problem in rust because we don't have inheritance.

IMO each type should explicitly state the excempt roles.

(However feel free to disagree and ignore).

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 breaking-change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants