Skip to main content

bitwarden_policies/
policy.rs

1//! The `Policy` trait and the enforcement machinery built on top of it.
2
3use std::collections::{HashMap, HashSet};
4
5use bitwarden_core::OrganizationId;
6use bitwarden_organizations::{OrganizationUserStatusType, OrganizationUserType};
7use serde::de::DeserializeOwned;
8
9use crate::{
10    OrganizationUserPolicyContext, PolicyView,
11    models::{EnforcedPolicy, EnforcedPolicyErased, ResolvedPolicyView},
12    policy_type::{PolicyDataType, PolicyType},
13};
14
15/// Strongly typed representation of a specific policy type in rust.
16///
17/// By implementing this, you define:
18/// - basic characteristics, such as the associated [`PolicyType`] and [`PolicyDataType`], and the
19///   configuration data struct (if any)
20/// - enforcement behavior, such as role exemptions.
21///
22/// The defaults match the most common Bitwarden policy: Provider users, owners and
23/// administrators are exempt, and policies only apply to Accepted and Confirmed members.
24pub(crate) trait Policy: Send + Sync + 'static {
25    /// Returns the policy type this definition handles.
26    fn policy_type(&self) -> PolicyType;
27
28    /// Erases the strongly-typed [`Data`](Self::Data) into the FFI-friendly
29    /// [`PolicyDataType`].
30    fn to_erased(&self, data: Self::Data) -> PolicyDataType;
31
32    /// The strongly-typed data for this policy. The [`Default`] value is
33    /// the fall-back whenever the policy is not enforced or the raw data could
34    /// not be parsed.
35    type Data: Default + DeserializeOwned;
36
37    /// Returns the organization roles that are exempt from this policy.
38    ///
39    /// Defaults to [`Owner`](OrganizationUserType::Owner) and
40    /// [`Admin`](OrganizationUserType::Admin).
41    fn exempt_roles(&self) -> &[OrganizationUserType] {
42        &[OrganizationUserType::Owner, OrganizationUserType::Admin]
43    }
44
45    /// Returns whether provider users are exempt from this policy.
46    ///
47    /// Defaults to `true`.
48    fn exempt_providers(&self) -> bool {
49        true
50    }
51
52    /// Returns the membership statuses that this policy should be enforced against.
53    ///
54    /// Defaults to [`Accepted`](OrganizationUserStatusType::Accepted) and
55    /// [`Confirmed`](OrganizationUserStatusType::Confirmed).
56    fn enforced_statuses(&self) -> &[OrganizationUserStatusType] {
57        &[
58            OrganizationUserStatusType::Accepted,
59            OrganizationUserStatusType::Confirmed,
60        ]
61    }
62}
63
64/// Evaluates whether a [`Policy`] is enforced against the current user.
65pub(crate) trait EnforceablePolicy: Policy {
66    /// Constructs a new [`EnforcedPolicy`] for a specific organization, evaluating
67    /// whether the policy should be enforced against the user or not.
68    ///
69    /// If the organization context is missing for the corresponding
70    /// organization, it will be enforced by default (err on the side of
71    /// enforcement).
72    fn get_enforced(
73        &self,
74        organization_id: OrganizationId,
75        policy_views: &[PolicyView],
76        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
77    ) -> EnforcedPolicy<Self>
78    where
79        Self: Sized;
80
81    /// Constructs a new [`EnforcedPolicy`] for all the user's organization, evaluating
82    /// whether each organization's policy should be enforced against the user or not.
83    ///
84    /// This will always return an [`EnforcedPolicy`] for each organization.
85    fn get_all_enforced(
86        &self,
87        policy_views: &[PolicyView],
88        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
89    ) -> Vec<EnforcedPolicy<Self>>
90    where
91        Self: Sized;
92}
93
94impl<P: Policy> EnforceablePolicy for P {
95    fn get_enforced(
96        &self,
97        organization_id: OrganizationId,
98        policy_views: &[PolicyView],
99        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
100    ) -> EnforcedPolicy<P> {
101        let resolved = policy_views
102            .iter()
103            .filter(|v| v.organization_id == organization_id)
104            .find_map(|v| ResolvedPolicyView::resolve(self, v));
105
106        match resolved {
107            // Matching policy of this type: evaluate
108            Some(resolved) => {
109                let contexts: HashMap<OrganizationId, &OrganizationUserPolicyContext> =
110                    organization_user_policy_contexts
111                        .iter()
112                        .map(|ctx| (ctx.id, ctx))
113                        .collect();
114
115                resolved.into_enforced(self, &contexts)
116            }
117            // No matching policy of this type: not enforced
118            None => EnforcedPolicy::not_enforced(organization_id),
119        }
120    }
121
122    fn get_all_enforced(
123        &self,
124        policy_views: &[PolicyView],
125        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
126    ) -> Vec<EnforcedPolicy<P>> {
127        // Evaluate policies: turn each policy into an EnforcedPolicy
128        let contexts: HashMap<OrganizationId, &OrganizationUserPolicyContext> =
129            organization_user_policy_contexts
130                .iter()
131                .map(|ctx| (ctx.id, ctx))
132                .collect();
133
134        let mut enforced_policies: Vec<EnforcedPolicy<P>> = policy_views
135            .iter()
136            .filter_map(|v| ResolvedPolicyView::resolve(self, v))
137            .map(|resolved| resolved.into_enforced(self, &contexts))
138            .collect();
139
140        // Evaluate organizations: for each organization without a policy, create an EnforcedPolicy
141        // for parity. This guarantees that every organization has an associated policy
142        // decision.
143        let context_organization_ids: HashSet<OrganizationId> = organization_user_policy_contexts
144            .iter()
145            .map(|c| c.id)
146            .collect();
147        let policy_organization_ids: HashSet<OrganizationId> = enforced_policies
148            .iter()
149            .map(|p| p.organization_id)
150            .collect();
151        let organizations_without_policies = context_organization_ids
152            .difference(&policy_organization_ids)
153            .map(|id| EnforcedPolicy::not_enforced(*id));
154
155        enforced_policies.extend(organizations_without_policies);
156        enforced_policies
157    }
158}
159
160/// Object-safe erasure of [`Policy`].
161///
162/// [`Policy`] cannot be used as a trait object because it has an associated
163/// [`Data`](Policy::Data) type. This trait hides that type behind the
164/// serializable [`PolicyDataType`], allowing evaluation of a `dyn Policy`.
165pub(crate) trait ErasedPolicy {
166    /// Type erased variant of [`EnforceablePolicy::get_enforced`].
167    fn get_enforced_erased(
168        &self,
169        organization_id: OrganizationId,
170        policy_views: &[PolicyView],
171        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
172    ) -> EnforcedPolicyErased;
173
174    /// Type erased variant of [`EnforceablePolicy::get_all_enforced`].
175    fn get_all_enforced_erased(
176        &self,
177        policy_views: &[PolicyView],
178        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
179    ) -> Vec<EnforcedPolicyErased>;
180}
181
182impl<P: Policy> ErasedPolicy for P {
183    fn get_enforced_erased(
184        &self,
185        organization_id: OrganizationId,
186        policy_views: &[PolicyView],
187        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
188    ) -> EnforcedPolicyErased {
189        self.get_enforced(
190            organization_id,
191            policy_views,
192            organization_user_policy_contexts,
193        )
194        .into_erased(self)
195    }
196
197    fn get_all_enforced_erased(
198        &self,
199        policy_views: &[PolicyView],
200        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
201    ) -> Vec<EnforcedPolicyErased> {
202        self.get_all_enforced(policy_views, organization_user_policy_contexts)
203            .into_iter()
204            .map(|decision| decision.into_erased(self))
205            .collect()
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use bitwarden_core::OrganizationId;
212    use uuid::Uuid;
213
214    use super::*;
215    use crate::{MasterPasswordPolicy, MasterPasswordPolicyResponse, policy_type::PolicyDataType};
216
217    /// A minimal policy with no data, used to exercise the enforcement gates
218    /// independently of any real policy's configuration. Overrides mirror the
219    /// trait defaults so the gate tests do not depend on them.
220    struct TestPolicy;
221    impl Policy for TestPolicy {
222        type Data = ();
223
224        fn policy_type(&self) -> PolicyType {
225            PolicyType::SingleOrg
226        }
227
228        fn to_erased(&self, _data: Self::Data) -> PolicyDataType {
229            PolicyDataType::SingleOrg
230        }
231
232        fn exempt_roles(&self) -> &[OrganizationUserType] {
233            &[OrganizationUserType::Owner, OrganizationUserType::Admin]
234        }
235
236        fn exempt_providers(&self) -> bool {
237            true
238        }
239
240        fn enforced_statuses(&self) -> &[OrganizationUserStatusType] {
241            &[
242                OrganizationUserStatusType::Accepted,
243                OrganizationUserStatusType::Confirmed,
244            ]
245        }
246    }
247
248    fn policy_view(
249        organization_id: OrganizationId,
250        policy_type: PolicyType,
251        enabled: bool,
252    ) -> PolicyView {
253        PolicyView {
254            id: Uuid::new_v4(),
255            organization_id,
256            r#type: policy_type,
257            data: None,
258            enabled,
259            revision_date: None,
260        }
261    }
262
263    /// A confirmed, enabled, non-provider member a policy applies to — the baseline for the gate
264    /// tests, which vary a single field via struct-update syntax, e.g.
265    /// `OrganizationUserPolicyContext { role: Owner, ..confirmed_member(org) }`.
266    fn confirmed_member(id: OrganizationId) -> OrganizationUserPolicyContext {
267        OrganizationUserPolicyContext {
268            id,
269            role: OrganizationUserType::User,
270            status: OrganizationUserStatusType::Confirmed,
271            enabled: true,
272            use_policies: true,
273            is_provider_user: false,
274        }
275    }
276
277    mod get_enforced {
278        use super::*;
279
280        /// Convenience for the single-org gate tests: resolves against the org of the first view.
281        /// Multi-org resolution is covered by
282        /// `get_all_enforced::resolves_each_org_independently`.
283        fn is_enforced(
284            org_id: OrganizationId,
285            views: &[PolicyView],
286            contexts: &[OrganizationUserPolicyContext],
287        ) -> bool {
288            TestPolicy.get_enforced(org_id, views, contexts).enforced
289        }
290
291        #[test]
292        fn enforced_for_confirmed_member() {
293            let org = OrganizationId::new_v4();
294            let views = [policy_view(org, PolicyType::SingleOrg, true)];
295            assert!(is_enforced(org, &views, &[confirmed_member(org)]));
296        }
297
298        #[test]
299        fn not_enforced_when_policy_disabled() {
300            let org = OrganizationId::new_v4();
301            let views = [policy_view(org, PolicyType::SingleOrg, false)];
302            assert!(!is_enforced(org, &views, &[confirmed_member(org)]));
303        }
304
305        #[test]
306        fn not_enforced_when_org_disabled() {
307            let org = OrganizationId::new_v4();
308            let views = [policy_view(org, PolicyType::SingleOrg, true)];
309            let ctx = OrganizationUserPolicyContext {
310                enabled: false,
311                ..confirmed_member(org)
312            };
313            assert!(!is_enforced(org, &views, &[ctx]));
314        }
315
316        #[test]
317        fn not_enforced_when_use_policies_false() {
318            let org = OrganizationId::new_v4();
319            let views = [policy_view(org, PolicyType::SingleOrg, true)];
320            let ctx = OrganizationUserPolicyContext {
321                use_policies: false,
322                ..confirmed_member(org)
323            };
324            assert!(!is_enforced(org, &views, &[ctx]));
325        }
326
327        #[test]
328        fn not_enforced_for_exempt_role() {
329            let org = OrganizationId::new_v4();
330            let views = [policy_view(org, PolicyType::SingleOrg, true)];
331            for (label, role) in [
332                ("Owner", OrganizationUserType::Owner),
333                ("Admin", OrganizationUserType::Admin),
334            ] {
335                let ctx = OrganizationUserPolicyContext {
336                    role,
337                    ..confirmed_member(org)
338                };
339                assert!(
340                    !is_enforced(org, &views, &[ctx]),
341                    "role {label} should be exempt"
342                );
343            }
344        }
345
346        #[test]
347        fn not_enforced_for_non_applicable_status() {
348            let org = OrganizationId::new_v4();
349            let views = [policy_view(org, PolicyType::SingleOrg, true)];
350            for (label, status) in [
351                ("Invited", OrganizationUserStatusType::Invited),
352                ("Revoked", OrganizationUserStatusType::Revoked),
353                ("Staged", OrganizationUserStatusType::Staged),
354            ] {
355                let ctx = OrganizationUserPolicyContext {
356                    status,
357                    ..confirmed_member(org)
358                };
359                assert!(
360                    !is_enforced(org, &views, &[ctx]),
361                    "status {label} should not be applicable"
362                );
363            }
364        }
365
366        #[test]
367        fn enforced_for_applicable_status() {
368            let org = OrganizationId::new_v4();
369            let views = [policy_view(org, PolicyType::SingleOrg, true)];
370            for (label, status) in [
371                ("Accepted", OrganizationUserStatusType::Accepted),
372                ("Confirmed", OrganizationUserStatusType::Confirmed),
373            ] {
374                let ctx = OrganizationUserPolicyContext {
375                    status,
376                    ..confirmed_member(org)
377                };
378                assert!(
379                    is_enforced(org, &views, &[ctx]),
380                    "status {label} should apply"
381                );
382            }
383        }
384
385        #[test]
386        fn not_enforced_for_provider_user() {
387            let org = OrganizationId::new_v4();
388            let views = [policy_view(org, PolicyType::SingleOrg, true)];
389            let ctx = OrganizationUserPolicyContext {
390                is_provider_user: true,
391                ..confirmed_member(org)
392            };
393            assert!(!is_enforced(org, &views, &[ctx]));
394        }
395
396        #[test]
397        fn wrong_policy_type_is_not_enforced() {
398            let org = OrganizationId::new_v4();
399            // A view for a different policy type must not resolve for TestPolicy.
400            let views = [policy_view(org, PolicyType::PasswordGenerator, true)];
401            assert!(!is_enforced(org, &views, &[confirmed_member(org)]));
402        }
403
404        #[test]
405        fn missing_org_context_enforces_enabled_policy_by_default() {
406            let org = OrganizationId::new_v4();
407            let views = [policy_view(org, PolicyType::SingleOrg, true)];
408            assert!(is_enforced(org, &views, &[]));
409        }
410
411        #[test]
412        fn missing_org_context_does_not_enforce_disabled_policy() {
413            let org = OrganizationId::new_v4();
414            let views = [policy_view(org, PolicyType::SingleOrg, false)];
415            assert!(!is_enforced(org, &views, &[]));
416        }
417
418        // --- Data parsing via `ResolvedPolicyView::resolve` (uses the real MasterPasswordPolicy)
419        // ---
420
421        fn mp_view(org: OrganizationId, data: Option<&str>) -> PolicyView {
422            PolicyView {
423                id: Uuid::new_v4(),
424                organization_id: org,
425                r#type: PolicyType::MasterPassword,
426                data: data.map(str::to_owned),
427                enabled: true,
428                revision_date: None,
429            }
430        }
431
432        #[test]
433        fn valid_data_is_parsed() {
434            let org = OrganizationId::new_v4();
435            let views = [mp_view(org, Some(r#"{"minComplexity":3,"minLength":12}"#))];
436            let decision = MasterPasswordPolicy.get_enforced(org, &views, &[confirmed_member(org)]);
437            assert!(decision.enforced);
438            assert_eq!(decision.data.min_complexity, Some(3));
439            assert_eq!(decision.data.min_length, Some(12));
440        }
441
442        #[test]
443        fn missing_data_falls_back_to_default() {
444            let org = OrganizationId::new_v4();
445            let views = [mp_view(org, None)];
446            let decision = MasterPasswordPolicy.get_enforced(org, &views, &[confirmed_member(org)]);
447            assert!(decision.enforced);
448            assert_eq!(decision.data, MasterPasswordPolicyResponse::default());
449        }
450
451        #[test]
452        fn malformed_data_falls_back_to_default_without_panicking() {
453            let org = OrganizationId::new_v4();
454            let views = [mp_view(org, Some("not json"))];
455            // Must not panic; the unparseable blob falls back to `Default` while the
456            // enforcement decision is still evaluated normally.
457            let decision = MasterPasswordPolicy.get_enforced(org, &views, &[confirmed_member(org)]);
458            assert!(decision.enforced);
459            assert_eq!(decision.data, MasterPasswordPolicyResponse::default());
460        }
461
462        #[test]
463        fn data_is_defaulted_when_not_enforced() {
464            let org = OrganizationId::new_v4();
465            let views = [mp_view(org, Some(r#"{"minComplexity":3}"#))];
466            // A revoked member: not enforced, so the parsed data is discarded in favor of the
467            // default.
468            let ctx = OrganizationUserPolicyContext {
469                status: OrganizationUserStatusType::Revoked,
470                ..confirmed_member(org)
471            };
472            let decision = MasterPasswordPolicy.get_enforced(org, &views, &[ctx]);
473            assert!(!decision.enforced);
474            assert_eq!(decision.data, MasterPasswordPolicyResponse::default());
475        }
476    }
477
478    mod get_all_enforced {
479        use super::*;
480
481        #[test]
482        fn resolves_each_org_independently() {
483            let org_a = OrganizationId::new_v4();
484            let org_b = OrganizationId::new_v4();
485            let views = [
486                policy_view(org_a, PolicyType::SingleOrg, true),
487                policy_view(org_b, PolicyType::SingleOrg, true),
488            ];
489            // org_a's member is a subject User; org_b's is an exempt Owner.
490            let contexts = [
491                confirmed_member(org_a),
492                OrganizationUserPolicyContext {
493                    role: OrganizationUserType::Owner,
494                    ..confirmed_member(org_b)
495                },
496            ];
497
498            // get_enforced selects the requested org's view and pairs it with that org's context,
499            // ignoring the other org entirely.
500            assert!(TestPolicy.get_enforced(org_a, &views, &contexts).enforced);
501            assert!(!TestPolicy.get_enforced(org_b, &views, &contexts).enforced);
502
503            // get_all_enforced yields one decision per view, each evaluated against its own org.
504            let all = TestPolicy.get_all_enforced(&views, &contexts);
505            assert_eq!(all.len(), 2);
506            assert!(
507                all.iter()
508                    .find(|d| d.organization_id == org_a)
509                    .expect("a decision for org_a")
510                    .enforced
511            );
512            assert!(
513                !all.iter()
514                    .find(|d| d.organization_id == org_b)
515                    .expect("a decision for org_b")
516                    .enforced
517            );
518        }
519
520        #[test]
521        fn given_organization_without_policy_returns_unenforced_policy() {
522            let org_a = OrganizationId::new_v4();
523            let org_b = OrganizationId::new_v4();
524            let org_c = OrganizationId::new_v4();
525
526            // Matching policy for org_a only. org_b has a different policy and org_c has no
527            // policies.
528            let views = [
529                policy_view(org_a, PolicyType::SingleOrg, true),
530                policy_view(org_b, PolicyType::MasterPassword, true),
531            ];
532
533            let contexts = [
534                confirmed_member(org_a),
535                confirmed_member(org_b),
536                confirmed_member(org_c),
537            ];
538
539            let result = TestPolicy.get_all_enforced(&views, &contexts);
540            assert!(result.len() == 3);
541            assert!(
542                result
543                    .iter()
544                    .find(|p| p.organization_id == org_a)
545                    .expect("a decision for org_a")
546                    .enforced
547            );
548            assert!(
549                !result
550                    .iter()
551                    .find(|p| p.organization_id == org_b)
552                    .expect("a decision for org_b")
553                    .enforced
554            );
555            assert!(
556                !result
557                    .iter()
558                    .find(|p| p.organization_id == org_c)
559                    .expect("a decision for org_c")
560                    .enforced
561            );
562        }
563    }
564}