Skip to content

Commit 291a28e

Browse files
authored
[PM-37933] refactor: Introduce OrganizationUserPolicyContext model (#1124)
The policies crate requires the full `ProfileOrganization` sync response model to be passed in for policy evaluation. However, that model is different between mobile and TS clients - the mobile sync model is much smaller because they need fewer admin-related properties. This tight coupling would require mobile to expand their organization model just to meet the interface, even though the logic doesn't actually need all these properties. The fix is to introduce a domain-specific `PolicyOrganizationContext` in `bitwarden-policies` and switch the filtering APIs and FFI wrapper to use it instead of the 61-field `ProfileOrganization`. The policy domain only needs 6 fields; this gives consumers a small, purpose-built input type to construct at the FFI boundary. This decoupling generally seems like better design for a library crate. ## 🚨 Breaking Changes The FFI signature of `filter_by_type` now takes `PolicyOrganizationContext` instead of `ProfileOrganization`. **No client is consuming this API yet**, so there is no migration impact in practice.
1 parent cc824dd commit 291a28e

8 files changed

Lines changed: 120 additions & 83 deletions

File tree

crates/bitwarden-policies/src/filter.rs

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,13 @@
77
88
use std::collections::HashMap;
99

10-
use bitwarden_organizations::{
11-
OrganizationUserStatusType, OrganizationUserType, ProfileOrganization,
12-
};
13-
use chrono::{DateTime, Utc};
14-
use serde::{Deserialize, Serialize};
15-
#[cfg(feature = "wasm")]
16-
use tsify::Tsify;
10+
use bitwarden_organizations::{OrganizationUserStatusType, OrganizationUserType};
1711
use uuid::Uuid;
1812

19-
use crate::policy_type::PolicyType;
20-
21-
/// An organization policy.
22-
#[allow(missing_docs)]
23-
#[derive(Serialize, Deserialize, Debug, Clone)]
24-
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
25-
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
26-
pub struct PolicyView {
27-
pub id: Uuid,
28-
pub organization_id: Uuid,
29-
pub r#type: PolicyType,
30-
/// The policy's raw configuration data as a JSON string, if any.
31-
pub data: Option<String>,
32-
pub enabled: bool,
33-
pub revision_date: Option<DateTime<Utc>>,
34-
}
13+
use crate::{
14+
models::{OrganizationUserPolicyContext, PolicyView},
15+
policy_type::PolicyType,
16+
};
3517

3618
/// Defines the filtering behavior for a specific policy type.
3719
///
@@ -75,15 +57,18 @@ pub trait PolicyFilter: Policy {
7557
/// This evaluates common business rules (e.g. the policy is enabled),
7658
/// as well as policy-specific rules according to its [`Policy`].
7759
///
78-
/// If a policy's organization is not present in `organizations`, the policy is enforced by
79-
/// default.
60+
/// If a policy's organization is not present in `organization_user_policy_contexts`, the policy
61+
/// is enforced by default.
8062
fn filter<'a>(
8163
&self,
8264
policies: &'a [PolicyView],
83-
organizations: &[ProfileOrganization],
65+
organization_user_policy_contexts: &[OrganizationUserPolicyContext],
8466
) -> Vec<&'a PolicyView> {
85-
let org_map: HashMap<&Uuid, &ProfileOrganization> =
86-
organizations.iter().map(|o| (&o.id, o)).collect();
67+
let org_map: HashMap<&Uuid, &OrganizationUserPolicyContext> =
68+
organization_user_policy_contexts
69+
.iter()
70+
.map(|o| (&o.id, o))
71+
.collect();
8772

8873
policies
8974
.iter()
@@ -95,7 +80,7 @@ pub trait PolicyFilter: Policy {
9580
org.enabled
9681
&& org.use_policies
9782
&& self.applicable_statuses().contains(&org.status)
98-
&& !self.exempt_roles().contains(&org.r#type)
83+
&& !self.exempt_roles().contains(&org.role)
9984
&& !(org.is_provider_user && self.exempt_providers())
10085
}
10186
None => true, // Unknown org: enforce by default
@@ -127,14 +112,14 @@ mod tests {
127112
user_type: OrganizationUserType,
128113
status: OrganizationUserStatusType,
129114
provider: bool,
130-
) -> ProfileOrganization {
131-
ProfileOrganization {
115+
) -> OrganizationUserPolicyContext {
116+
OrganizationUserPolicyContext {
132117
id,
133-
r#type: user_type,
118+
role: user_type,
134119
status,
120+
enabled: true,
135121
use_policies: true,
136122
is_provider_user: provider,
137-
..Default::default()
138123
}
139124
}
140125

@@ -180,14 +165,13 @@ mod tests {
180165
#[test]
181166
fn disabled_organization_is_filtered_out() {
182167
let org_id = Uuid::new_v4();
183-
let orgs = [ProfileOrganization {
168+
let orgs = [OrganizationUserPolicyContext {
184169
enabled: false,
185170
id: org_id,
186-
r#type: OrganizationUserType::User,
171+
role: OrganizationUserType::User,
187172
status: OrganizationUserStatusType::Confirmed,
188173
use_policies: true,
189174
is_provider_user: false,
190-
..Default::default()
191175
}];
192176
let policies = [policy_view(org_id, PolicyType::MasterPassword, true)];
193177

@@ -228,13 +212,13 @@ mod tests {
228212
#[test]
229213
fn use_policies_false_is_filtered_out() {
230214
let org_id = Uuid::new_v4();
231-
let orgs = [ProfileOrganization {
215+
let orgs = [OrganizationUserPolicyContext {
232216
id: org_id,
233-
r#type: OrganizationUserType::User,
217+
role: OrganizationUserType::User,
234218
status: OrganizationUserStatusType::Confirmed,
219+
enabled: true,
235220
use_policies: false,
236221
is_provider_user: false,
237-
..Default::default()
238222
}];
239223
let policies = [policy_view(org_id, PolicyType::MasterPassword, true)];
240224

crates/bitwarden-policies/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ mod uniffi_support;
77

88
pub mod filter;
99
mod master_password_policy_response;
10+
mod models;
1011
mod policy_client;
1112
pub mod policy_overrides;
1213
mod policy_type;
1314
mod registry;
1415

15-
pub use filter::{Policy, PolicyView};
16+
pub use filter::Policy;
1617
pub use master_password_policy_response::MasterPasswordPolicyResponse;
18+
pub use models::{OrganizationUserPolicyContext, PolicyView};
1719
pub use policy_client::{PoliciesClientExt, PolicyClient};
1820
pub use policy_overrides::*;
1921
pub use policy_type::PolicyType;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//! Data models for the policy domain.
2+
//!
3+
//! These are the inputs to the policy filtering API and are exposed across the
4+
//! FFI boundary.
5+
6+
use bitwarden_organizations::{OrganizationUserStatusType, OrganizationUserType};
7+
use chrono::{DateTime, Utc};
8+
use serde::{Deserialize, Serialize};
9+
#[cfg(feature = "wasm")]
10+
use tsify::Tsify;
11+
use uuid::Uuid;
12+
13+
use crate::policy_type::PolicyType;
14+
15+
/// An organization policy.
16+
#[derive(Serialize, Deserialize, Debug, Clone)]
17+
#[serde(rename_all = "camelCase")]
18+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
19+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
20+
pub struct PolicyView {
21+
/// The policy's unique ID.
22+
pub id: Uuid,
23+
/// The organization this policy belongs to.
24+
pub organization_id: Uuid,
25+
/// The type of policy.
26+
pub r#type: PolicyType,
27+
/// The policy's additional configuration data as a JSON string, if any.
28+
pub data: Option<String>,
29+
/// Whether the policy is enabled.
30+
pub enabled: bool,
31+
/// When the policy was last modified.
32+
pub revision_date: Option<DateTime<Utc>>,
33+
}
34+
35+
/// A minimal set of data for a user in an organization. This provides
36+
/// the context needed to evaluate the policies that are applied to the
37+
/// user by the organization.
38+
#[derive(Serialize, Deserialize, Debug, Clone)]
39+
#[serde(rename_all = "camelCase")]
40+
#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
41+
#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
42+
pub struct OrganizationUserPolicyContext {
43+
/// The organization's unique ID.
44+
pub id: Uuid,
45+
/// The user's membership status in the organization.
46+
pub status: OrganizationUserStatusType,
47+
/// The user's role within the organization.
48+
pub role: OrganizationUserType,
49+
/// Whether the organization is enabled.
50+
pub enabled: bool,
51+
/// Whether the organization's plan supports policies.
52+
pub use_policies: bool,
53+
/// Whether the user is acting on behalf of a provider
54+
/// that manages the organization.
55+
pub is_provider_user: bool,
56+
}

crates/bitwarden-policies/src/policy_client.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
//! [`PolicyClient`] and its associated extension trait.
22
33
use bitwarden_core::Client;
4-
use bitwarden_organizations::ProfileOrganization;
54
#[cfg(feature = "wasm")]
65
use wasm_bindgen::prelude::wasm_bindgen;
76

8-
use crate::{PolicyType, filter::PolicyView, policy_overrides::*, registry::PolicyRegistry};
7+
use crate::{
8+
OrganizationUserPolicyContext, PolicyType, PolicyView, policy_overrides::*,
9+
registry::PolicyRegistry,
10+
};
911

1012
fn build_policy_registry() -> PolicyRegistry {
1113
PolicyRegistry::builder()
@@ -51,11 +53,11 @@ impl PolicyClient {
5153
pub fn filter_by_type(
5254
&self,
5355
policies: Vec<PolicyView>,
54-
organizations: Vec<ProfileOrganization>,
56+
organization_user_policy_contexts: Vec<OrganizationUserPolicyContext>,
5557
policy_type: PolicyType,
5658
) -> Vec<PolicyView> {
5759
self.registry
58-
.filter_by_type(&policies, &organizations, policy_type)
60+
.filter_by_type(&policies, &organization_user_policy_contexts, policy_type)
5961
.into_iter()
6062
.cloned()
6163
.collect()
@@ -93,14 +95,14 @@ mod tests {
9395
}
9496
}
9597

96-
fn organization(id: Uuid) -> ProfileOrganization {
97-
ProfileOrganization {
98+
fn organization(id: Uuid) -> OrganizationUserPolicyContext {
99+
OrganizationUserPolicyContext {
98100
id,
99-
r#type: OrganizationUserType::User,
101+
role: OrganizationUserType::User,
100102
status: OrganizationUserStatusType::Confirmed,
103+
enabled: true,
101104
use_policies: true,
102105
is_provider_user: false,
103-
..Default::default()
104106
}
105107
}
106108

@@ -147,13 +149,13 @@ mod tests {
147149
let org_id = Uuid::new_v4();
148150
// Owner — normally exempt, but NoExemptionPolicy removes the exemption
149151
let policies = vec![policy_view(org_id, PolicyType::MasterPassword, true)];
150-
let orgs = vec![ProfileOrganization {
152+
let orgs = vec![OrganizationUserPolicyContext {
151153
id: org_id,
152-
r#type: OrganizationUserType::Owner,
154+
role: OrganizationUserType::Owner,
153155
status: OrganizationUserStatusType::Confirmed,
156+
enabled: true,
154157
use_policies: true,
155158
is_provider_user: false,
156-
..Default::default()
157159
}];
158160

159161
let registry = PolicyRegistry::builder()

crates/bitwarden-policies/src/policy_overrides.rs

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ pub struct FreeFamiliesSponsorshipPolicy;
5656

5757
impl Policy for FreeFamiliesSponsorshipPolicy {
5858
fn policy_type(&self) -> PolicyType {
59-
PolicyType::FreeFamiliesSponsorshipPolicy
59+
PolicyType::FreeFamiliesSponsorship
6060
}
6161

6262
fn exempt_roles(&self) -> &[OrganizationUserType] {
@@ -86,7 +86,7 @@ pub struct RestrictedItemTypesPolicy;
8686

8787
impl Policy for RestrictedItemTypesPolicy {
8888
fn policy_type(&self) -> PolicyType {
89-
PolicyType::RestrictedItemTypesPolicy
89+
PolicyType::RestrictedItemTypes
9090
}
9191

9292
fn exempt_roles(&self) -> &[OrganizationUserType] {
@@ -111,13 +111,11 @@ impl Policy for AutomaticUserConfirmationPolicy {
111111

112112
#[cfg(test)]
113113
mod tests {
114-
use bitwarden_organizations::{
115-
OrganizationUserStatusType, OrganizationUserType, ProfileOrganization,
116-
};
114+
use bitwarden_organizations::{OrganizationUserStatusType, OrganizationUserType};
117115
use uuid::Uuid;
118116

119117
use super::*;
120-
use crate::filter::{PolicyFilter, PolicyView};
118+
use crate::{OrganizationUserPolicyContext, PolicyView, filter::PolicyFilter};
121119

122120
fn policy_view(organization_id: Uuid, policy_type: PolicyType) -> PolicyView {
123121
PolicyView {
@@ -130,14 +128,14 @@ mod tests {
130128
}
131129
}
132130

133-
fn org(id: Uuid, user_type: OrganizationUserType) -> ProfileOrganization {
134-
ProfileOrganization {
131+
fn org(id: Uuid, user_type: OrganizationUserType) -> OrganizationUserPolicyContext {
132+
OrganizationUserPolicyContext {
135133
id,
136-
r#type: user_type,
134+
role: user_type,
137135
status: OrganizationUserStatusType::Confirmed,
136+
enabled: true,
138137
use_policies: true,
139138
is_provider_user: false,
140-
..Default::default()
141139
}
142140
}
143141

@@ -212,10 +210,7 @@ mod tests {
212210
#[test]
213211
fn free_families_applies_to_owner() {
214212
let org_id = Uuid::new_v4();
215-
let policies = [policy_view(
216-
org_id,
217-
PolicyType::FreeFamiliesSponsorshipPolicy,
218-
)];
213+
let policies = [policy_view(org_id, PolicyType::FreeFamiliesSponsorship)];
219214
let orgs = [org(org_id, OrganizationUserType::Owner)];
220215
assert_eq!(
221216
FreeFamiliesSponsorshipPolicy.filter(&policies, &orgs).len(),
@@ -238,7 +233,7 @@ mod tests {
238233
#[test]
239234
fn restricted_item_types_applies_to_owner() {
240235
let org_id = Uuid::new_v4();
241-
let policies = [policy_view(org_id, PolicyType::RestrictedItemTypesPolicy)];
236+
let policies = [policy_view(org_id, PolicyType::RestrictedItemTypes)];
242237
let orgs = [org(org_id, OrganizationUserType::Owner)];
243238
assert_eq!(RestrictedItemTypesPolicy.filter(&policies, &orgs).len(), 1);
244239
}

crates/bitwarden-policies/src/policy_type.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ pub enum PolicyType {
4646
/// Automatically logs members into apps using single sign-on.
4747
AutomaticAppLogIn = 12,
4848
/// Removes members' access to the free Bitwarden Families sponsorship benefit.
49-
FreeFamiliesSponsorshipPolicy = 13,
49+
FreeFamiliesSponsorship = 13,
5050
/// Prevents members from unlocking the app with a PIN.
5151
RemoveUnlockWithPin = 14,
5252
/// Restricts the item types that members can create.
53-
RestrictedItemTypesPolicy = 15,
53+
RestrictedItemTypes = 15,
5454
/// Sets the default URI match detection strategy for autofill.
5555
UriMatchDefaults = 16,
5656
/// Sets the default behavior for the autotype feature.

crates/bitwarden-policies/src/registry.rs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@
88
99
use std::collections::HashMap;
1010

11-
use bitwarden_organizations::ProfileOrganization;
12-
1311
use crate::{
14-
PolicyType,
15-
filter::{Policy, PolicyFilter, PolicyView},
12+
OrganizationUserPolicyContext, PolicyType, PolicyView,
13+
filter::{Policy, PolicyFilter},
1614
};
1715

1816
/// A [`Policy`] that uses the default filtering behavior for any policy type.
@@ -50,12 +48,12 @@ impl PolicyRegistry {
5048
pub(crate) fn filter_by_type<'a>(
5149
&self,
5250
policies: &'a [PolicyView],
53-
organizations: &[ProfileOrganization],
51+
organization_user_policy_contexts: &[OrganizationUserPolicyContext],
5452
policy_type: PolicyType,
5553
) -> Vec<&'a PolicyView> {
5654
match self.policies.get(&policy_type) {
57-
Some(p) => p.filter(policies, organizations),
58-
None => DefaultPolicy(policy_type).filter(policies, organizations),
55+
Some(p) => p.filter(policies, organization_user_policy_contexts),
56+
None => DefaultPolicy(policy_type).filter(policies, organization_user_policy_contexts),
5957
}
6058
}
6159
}
@@ -111,14 +109,14 @@ mod tests {
111109
user_type: OrganizationUserType,
112110
status: OrganizationUserStatusType,
113111
provider: bool,
114-
) -> ProfileOrganization {
115-
ProfileOrganization {
112+
) -> OrganizationUserPolicyContext {
113+
OrganizationUserPolicyContext {
116114
id,
117-
r#type: user_type,
115+
role: user_type,
118116
status,
117+
enabled: true,
119118
use_policies: true,
120119
is_provider_user: provider,
121-
..Default::default()
122120
}
123121
}
124122

0 commit comments

Comments
 (0)