Skip to main content

bitwarden_policies/
policy_definition.rs

1//! The `PolicyDefinition` 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, Policy,
11    models::{PolicyDecision, PolicyDecisionErased, PolicyView},
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 PolicyDefinition: 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 [`PolicyDefinition`] is enforced against the current user.
65pub(crate) trait EnforceablePolicy: PolicyDefinition {
66    /// Constructs a new [`PolicyDecision`] 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        policies: &[Policy],
76        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
77    ) -> PolicyDecision<Self>
78    where
79        Self: Sized;
80
81    /// Constructs a new [`PolicyDecision`] 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 a [`PolicyDecision`] for each organization.
85    fn get_all_enforced(
86        &self,
87        policies: &[Policy],
88        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
89    ) -> Vec<PolicyDecision<Self>>
90    where
91        Self: Sized;
92}
93
94impl<P: PolicyDefinition> EnforceablePolicy for P {
95    fn get_enforced(
96        &self,
97        organization_id: OrganizationId,
98        policies: &[Policy],
99        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
100    ) -> PolicyDecision<P> {
101        let resolved = policies
102            .iter()
103            .filter(|v| v.organization_id == organization_id)
104            .find_map(|v| PolicyView::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 => PolicyDecision::not_enforced(organization_id),
119        }
120    }
121
122    fn get_all_enforced(
123        &self,
124        policies: &[Policy],
125        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
126    ) -> Vec<PolicyDecision<P>> {
127        // Evaluate policies: turn each policy into a PolicyDecision
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<PolicyDecision<P>> = policies
135            .iter()
136            .filter_map(|v| PolicyView::resolve(self, v))
137            .map(|resolved| resolved.into_enforced(self, &contexts))
138            .collect();
139
140        // Evaluate organizations: for each organization without a policy, create a PolicyDecision
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| PolicyDecision::not_enforced(*id));
154
155        enforced_policies.extend(organizations_without_policies);
156        enforced_policies
157    }
158}
159
160/// Object-safe erasure of [`PolicyDefinition`].
161///
162/// [`PolicyDefinition`] cannot be used as a trait object because it has an associated
163/// [`Data`](PolicyDefinition::Data) type. This trait hides that type behind the
164/// serializable [`PolicyDataType`], allowing evaluation of a `dyn PolicyDefinition`.
165pub(crate) trait ErasedPolicy {
166    /// Type erased variant of [`EnforceablePolicy::get_enforced`].
167    fn get_enforced_erased(
168        &self,
169        organization_id: OrganizationId,
170        policies: &[Policy],
171        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
172    ) -> PolicyDecisionErased;
173
174    /// Type erased variant of [`EnforceablePolicy::get_all_enforced`].
175    fn get_all_enforced_erased(
176        &self,
177        policies: &[Policy],
178        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
179    ) -> Vec<PolicyDecisionErased>;
180}
181
182impl<P: PolicyDefinition> ErasedPolicy for P {
183    fn get_enforced_erased(
184        &self,
185        organization_id: OrganizationId,
186        policies: &[Policy],
187        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
188    ) -> PolicyDecisionErased {
189        self.get_enforced(organization_id, policies, organization_user_policy_contexts)
190            .into_erased(self)
191    }
192
193    fn get_all_enforced_erased(
194        &self,
195        policies: &[Policy],
196        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
197    ) -> Vec<PolicyDecisionErased> {
198        self.get_all_enforced(policies, organization_user_policy_contexts)
199            .into_iter()
200            .map(|decision| decision.into_erased(self))
201            .collect()
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use bitwarden_core::OrganizationId;
208
209    use super::*;
210    use crate::{
211        MasterPasswordPolicy, MasterPasswordPolicyData, PolicyId, policy_type::PolicyDataType,
212    };
213
214    /// A minimal policy with no data, used to exercise the enforcement gates
215    /// independently of any real policy's configuration. Overrides mirror the
216    /// trait defaults so the gate tests do not depend on them.
217    struct TestPolicy;
218    impl PolicyDefinition for TestPolicy {
219        type Data = ();
220
221        fn policy_type(&self) -> PolicyType {
222            PolicyType::SingleOrg
223        }
224
225        fn to_erased(&self, _data: Self::Data) -> PolicyDataType {
226            PolicyDataType::SingleOrg
227        }
228
229        fn exempt_roles(&self) -> &[OrganizationUserType] {
230            &[OrganizationUserType::Owner, OrganizationUserType::Admin]
231        }
232
233        fn exempt_providers(&self) -> bool {
234            true
235        }
236
237        fn enforced_statuses(&self) -> &[OrganizationUserStatusType] {
238            &[
239                OrganizationUserStatusType::Accepted,
240                OrganizationUserStatusType::Confirmed,
241            ]
242        }
243    }
244
245    fn policy(organization_id: OrganizationId, policy_type: PolicyType, enabled: bool) -> Policy {
246        Policy {
247            id: PolicyId::new_v4(),
248            organization_id,
249            r#type: policy_type,
250            data: None,
251            enabled,
252            revision_date: None,
253        }
254    }
255
256    /// A confirmed, enabled, non-provider member a policy applies to — the baseline for the gate
257    /// tests, which vary a single field via struct-update syntax, e.g.
258    /// `OrganizationUserPolicyContext { role: Owner, ..confirmed_member(org) }`.
259    fn confirmed_member(id: OrganizationId) -> OrganizationUserPolicyContext {
260        OrganizationUserPolicyContext {
261            id,
262            role: OrganizationUserType::User,
263            status: OrganizationUserStatusType::Confirmed,
264            enabled: true,
265            use_policies: true,
266            is_provider_user: false,
267        }
268    }
269
270    mod get_enforced {
271        use super::*;
272
273        /// Convenience for the single-org gate tests: resolves against the org of the first view.
274        /// Multi-org resolution is covered by
275        /// `get_all_enforced::resolves_each_org_independently`.
276        fn is_enforced(
277            org_id: OrganizationId,
278            views: &[Policy],
279            contexts: &[OrganizationUserPolicyContext],
280        ) -> bool {
281            TestPolicy.get_enforced(org_id, views, contexts).enforced
282        }
283
284        #[test]
285        fn enforced_for_confirmed_member() {
286            let org = OrganizationId::new_v4();
287            let views = [policy(org, PolicyType::SingleOrg, true)];
288            assert!(is_enforced(org, &views, &[confirmed_member(org)]));
289        }
290
291        #[test]
292        fn not_enforced_when_policy_disabled() {
293            let org = OrganizationId::new_v4();
294            let views = [policy(org, PolicyType::SingleOrg, false)];
295            assert!(!is_enforced(org, &views, &[confirmed_member(org)]));
296        }
297
298        #[test]
299        fn not_enforced_when_org_disabled() {
300            let org = OrganizationId::new_v4();
301            let views = [policy(org, PolicyType::SingleOrg, true)];
302            let ctx = OrganizationUserPolicyContext {
303                enabled: false,
304                ..confirmed_member(org)
305            };
306            assert!(!is_enforced(org, &views, &[ctx]));
307        }
308
309        #[test]
310        fn not_enforced_when_use_policies_false() {
311            let org = OrganizationId::new_v4();
312            let views = [policy(org, PolicyType::SingleOrg, true)];
313            let ctx = OrganizationUserPolicyContext {
314                use_policies: false,
315                ..confirmed_member(org)
316            };
317            assert!(!is_enforced(org, &views, &[ctx]));
318        }
319
320        #[test]
321        fn not_enforced_for_exempt_role() {
322            let org = OrganizationId::new_v4();
323            let views = [policy(org, PolicyType::SingleOrg, true)];
324            for (label, role) in [
325                ("Owner", OrganizationUserType::Owner),
326                ("Admin", OrganizationUserType::Admin),
327            ] {
328                let ctx = OrganizationUserPolicyContext {
329                    role,
330                    ..confirmed_member(org)
331                };
332                assert!(
333                    !is_enforced(org, &views, &[ctx]),
334                    "role {label} should be exempt"
335                );
336            }
337        }
338
339        #[test]
340        fn not_enforced_for_non_applicable_status() {
341            let org = OrganizationId::new_v4();
342            let views = [policy(org, PolicyType::SingleOrg, true)];
343            for (label, status) in [
344                ("Invited", OrganizationUserStatusType::Invited),
345                ("Revoked", OrganizationUserStatusType::Revoked),
346                ("Staged", OrganizationUserStatusType::Staged),
347            ] {
348                let ctx = OrganizationUserPolicyContext {
349                    status,
350                    ..confirmed_member(org)
351                };
352                assert!(
353                    !is_enforced(org, &views, &[ctx]),
354                    "status {label} should not be applicable"
355                );
356            }
357        }
358
359        #[test]
360        fn enforced_for_applicable_status() {
361            let org = OrganizationId::new_v4();
362            let views = [policy(org, PolicyType::SingleOrg, true)];
363            for (label, status) in [
364                ("Accepted", OrganizationUserStatusType::Accepted),
365                ("Confirmed", OrganizationUserStatusType::Confirmed),
366            ] {
367                let ctx = OrganizationUserPolicyContext {
368                    status,
369                    ..confirmed_member(org)
370                };
371                assert!(
372                    is_enforced(org, &views, &[ctx]),
373                    "status {label} should apply"
374                );
375            }
376        }
377
378        #[test]
379        fn not_enforced_for_provider_user() {
380            let org = OrganizationId::new_v4();
381            let views = [policy(org, PolicyType::SingleOrg, true)];
382            let ctx = OrganizationUserPolicyContext {
383                is_provider_user: true,
384                ..confirmed_member(org)
385            };
386            assert!(!is_enforced(org, &views, &[ctx]));
387        }
388
389        #[test]
390        fn wrong_policy_type_is_not_enforced() {
391            let org = OrganizationId::new_v4();
392            // A view for a different policy type must not resolve for TestPolicy.
393            let views = [policy(org, PolicyType::PasswordGenerator, true)];
394            assert!(!is_enforced(org, &views, &[confirmed_member(org)]));
395        }
396
397        #[test]
398        fn missing_org_context_enforces_enabled_policy_by_default() {
399            let org = OrganizationId::new_v4();
400            let views = [policy(org, PolicyType::SingleOrg, true)];
401            assert!(is_enforced(org, &views, &[]));
402        }
403
404        #[test]
405        fn missing_org_context_does_not_enforce_disabled_policy() {
406            let org = OrganizationId::new_v4();
407            let views = [policy(org, PolicyType::SingleOrg, false)];
408            assert!(!is_enforced(org, &views, &[]));
409        }
410
411        // --- Data parsing via `PolicyView::resolve` (uses the real MasterPasswordPolicy)
412        // ---
413
414        fn mp_view(org: OrganizationId, data: Option<&str>) -> Policy {
415            Policy {
416                id: PolicyId::new_v4(),
417                organization_id: org,
418                r#type: PolicyType::MasterPassword,
419                data: data.map(str::to_owned),
420                enabled: true,
421                revision_date: None,
422            }
423        }
424
425        #[test]
426        fn valid_data_is_parsed() {
427            let org = OrganizationId::new_v4();
428            let views = [mp_view(org, Some(r#"{"minComplexity":3,"minLength":12}"#))];
429            let decision = MasterPasswordPolicy.get_enforced(org, &views, &[confirmed_member(org)]);
430            assert!(decision.enforced);
431            assert_eq!(decision.data.min_complexity, Some(3));
432            assert_eq!(decision.data.min_length, Some(12));
433        }
434
435        #[test]
436        fn missing_data_falls_back_to_default() {
437            let org = OrganizationId::new_v4();
438            let views = [mp_view(org, None)];
439            let decision = MasterPasswordPolicy.get_enforced(org, &views, &[confirmed_member(org)]);
440            assert!(decision.enforced);
441            assert_eq!(decision.data, MasterPasswordPolicyData::default());
442        }
443
444        #[test]
445        fn malformed_data_falls_back_to_default_without_panicking() {
446            let org = OrganizationId::new_v4();
447            let views = [mp_view(org, Some("not json"))];
448            // Must not panic; the unparseable blob falls back to `Default` while the
449            // enforcement decision is still evaluated normally.
450            let decision = MasterPasswordPolicy.get_enforced(org, &views, &[confirmed_member(org)]);
451            assert!(decision.enforced);
452            assert_eq!(decision.data, MasterPasswordPolicyData::default());
453        }
454
455        #[test]
456        fn data_is_defaulted_when_not_enforced() {
457            let org = OrganizationId::new_v4();
458            let views = [mp_view(org, Some(r#"{"minComplexity":3}"#))];
459            // A revoked member: not enforced, so the parsed data is discarded in favor of the
460            // default.
461            let ctx = OrganizationUserPolicyContext {
462                status: OrganizationUserStatusType::Revoked,
463                ..confirmed_member(org)
464            };
465            let decision = MasterPasswordPolicy.get_enforced(org, &views, &[ctx]);
466            assert!(!decision.enforced);
467            assert_eq!(decision.data, MasterPasswordPolicyData::default());
468        }
469
470        // --- Data parsing for the more complex data-carrying policies ---
471
472        fn typed_view(org: OrganizationId, policy_type: PolicyType, data: &str) -> Policy {
473            Policy {
474                id: PolicyId::new_v4(),
475                organization_id: org,
476                r#type: policy_type,
477                data: Some(data.to_owned()),
478                enabled: true,
479                revision_date: None,
480            }
481        }
482
483        #[test]
484        fn maximum_vault_timeout_data_is_parsed() {
485            use crate::{MaximumVaultTimeoutPolicy, VaultTimeoutAction, VaultTimeoutType};
486
487            let org = OrganizationId::new_v4();
488            let views = [typed_view(
489                org,
490                PolicyType::MaximumVaultTimeout,
491                r#"{"type":"custom","minutes":480,"action":"logOut"}"#,
492            )];
493            let decision =
494                MaximumVaultTimeoutPolicy.get_enforced(org, &views, &[confirmed_member(org)]);
495            assert!(decision.enforced);
496            assert_eq!(decision.data.timeout_type, Some(VaultTimeoutType::Custom));
497            assert_eq!(decision.data.minutes, Some(480));
498            assert_eq!(decision.data.action, Some(VaultTimeoutAction::LogOut));
499        }
500
501        #[test]
502        fn password_generator_data_is_parsed() {
503            use crate::{PasswordGeneratorPolicy, PasswordGeneratorType};
504
505            let org = OrganizationId::new_v4();
506            let views = [typed_view(
507                org,
508                PolicyType::PasswordGenerator,
509                r#"{"overridePasswordType":"passphrase","minLength":14,"capitalize":true}"#,
510            )];
511            let decision =
512                PasswordGeneratorPolicy.get_enforced(org, &views, &[confirmed_member(org)]);
513            assert!(decision.enforced);
514            assert_eq!(
515                decision.data.override_password_type,
516                Some(PasswordGeneratorType::Passphrase)
517            );
518            assert_eq!(decision.data.min_length, Some(14));
519            assert_eq!(decision.data.capitalize, Some(true));
520        }
521    }
522
523    mod get_all_enforced {
524        use super::*;
525
526        #[test]
527        fn resolves_each_org_independently() {
528            let org_a = OrganizationId::new_v4();
529            let org_b = OrganizationId::new_v4();
530            let views = [
531                policy(org_a, PolicyType::SingleOrg, true),
532                policy(org_b, PolicyType::SingleOrg, true),
533            ];
534            // org_a's member is a subject User; org_b's is an exempt Owner.
535            let contexts = [
536                confirmed_member(org_a),
537                OrganizationUserPolicyContext {
538                    role: OrganizationUserType::Owner,
539                    ..confirmed_member(org_b)
540                },
541            ];
542
543            // get_enforced selects the requested org's view and pairs it with that org's context,
544            // ignoring the other org entirely.
545            assert!(TestPolicy.get_enforced(org_a, &views, &contexts).enforced);
546            assert!(!TestPolicy.get_enforced(org_b, &views, &contexts).enforced);
547
548            // get_all_enforced yields one decision per view, each evaluated against its own org.
549            let all = TestPolicy.get_all_enforced(&views, &contexts);
550            assert_eq!(all.len(), 2);
551            assert!(
552                all.iter()
553                    .find(|d| d.organization_id == org_a)
554                    .expect("a decision for org_a")
555                    .enforced
556            );
557            assert!(
558                !all.iter()
559                    .find(|d| d.organization_id == org_b)
560                    .expect("a decision for org_b")
561                    .enforced
562            );
563        }
564
565        #[test]
566        fn given_organization_without_policy_returns_unenforced_policy() {
567            let org_a = OrganizationId::new_v4();
568            let org_b = OrganizationId::new_v4();
569            let org_c = OrganizationId::new_v4();
570
571            // Matching policy for org_a only. org_b has a different policy and org_c has no
572            // policies.
573            let views = [
574                policy(org_a, PolicyType::SingleOrg, true),
575                policy(org_b, PolicyType::MasterPassword, true),
576            ];
577
578            let contexts = [
579                confirmed_member(org_a),
580                confirmed_member(org_b),
581                confirmed_member(org_c),
582            ];
583
584            let result = TestPolicy.get_all_enforced(&views, &contexts);
585            assert!(result.len() == 3);
586            assert!(
587                result
588                    .iter()
589                    .find(|p| p.organization_id == org_a)
590                    .expect("a decision for org_a")
591                    .enforced
592            );
593            assert!(
594                !result
595                    .iter()
596                    .find(|p| p.organization_id == org_b)
597                    .expect("a decision for org_b")
598                    .enforced
599            );
600            assert!(
601                !result
602                    .iter()
603                    .find(|p| p.organization_id == org_c)
604                    .expect("a decision for org_c")
605                    .enforced
606            );
607        }
608    }
609}