Skip to main content

bitwarden_policies/
policy_client.rs

1//! [`PolicyClient`] and its associated extension trait.
2
3use std::collections::HashMap;
4
5use bitwarden_core::{Client, OrganizationId};
6
7use crate::{OrganizationUserPolicyContext, Policy, PolicyType, models::PolicyDecisionErased};
8// The strongly-typed native enforcement API is test-only for now: it is exercised by tests but
9// not yet exposed to consumers. See the `#[cfg(test)]` impl block below.
10#[cfg(test)]
11use crate::{PolicyDefinition, models::PolicyDecision, policy_definition::EnforceablePolicy};
12
13/// Client for policy domain operations.
14///
15/// Obtained via [`PoliciesClientExt::policies`] on a [`Client`].
16#[bitwarden_ffi::wasm_object]
17pub struct PolicyClient;
18
19impl Default for PolicyClient {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25impl PolicyClient {
26    /// Creates a new [`PolicyClient`].
27    pub fn new() -> Self {
28        Self
29    }
30}
31
32/// FFI client using type erasure to cross the boundary.
33/// Native rust callers should use the non-erased interfaces instead.
34#[bitwarden_ffi::wasm_export]
35impl PolicyClient {
36    /// Evaluate enforcement of the given policy type across all organizations,
37    /// returning type-erased decisions for the FFI boundary.
38    ///
39    /// Not yet intended for consumer use: exposed across the FFI boundary for
40    /// testing and iteration only. Use [`filter_by_type`](Self::filter_by_type) for now.
41    #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "get_all_enforced"))]
42    pub fn get_all_enforced_erased(
43        &self,
44        policy_type: PolicyType,
45        // TODO: policies and ctx should come from state rather than being specified by the
46        // caller
47        policies: Vec<Policy>,
48        organization_user_policy_contexts: Vec<OrganizationUserPolicyContext>,
49    ) -> Vec<PolicyDecisionErased> {
50        policy_type
51            .resolve_policy()
52            .get_all_enforced_erased(&policies, &organization_user_policy_contexts)
53    }
54
55    /// Evaluate enforcement of the given policy type for a single organization,
56    /// returning a type-erased decision for the FFI boundary.
57    ///
58    /// Not yet intended for consumer use: exposed across the FFI boundary for
59    /// testing only. Use [`filter_by_type`](Self::filter_by_type) for now.
60    #[cfg_attr(feature = "wasm", wasm_bindgen(js_name = "get_enforced"))]
61    pub fn get_enforced_erased(
62        &self,
63        policy_type: PolicyType,
64        organization_id: OrganizationId,
65        // TODO: policies and ctx should come from state rather than being specified by the
66        // caller
67        policies: Vec<Policy>,
68        organization_user_policy_contexts: Vec<OrganizationUserPolicyContext>,
69    ) -> PolicyDecisionErased {
70        policy_type.resolve_policy().get_enforced_erased(
71            organization_id,
72            &policies,
73            &organization_user_policy_contexts,
74        )
75    }
76
77    /// Filter policies of the given type for the current user.
78    pub fn filter_by_type(
79        &self,
80        policies: Vec<Policy>,
81        organization_user_policy_contexts: Vec<OrganizationUserPolicyContext>,
82        policy_type: PolicyType,
83    ) -> Vec<Policy> {
84        // Use the enforced path as the canonical logic, then use it to filter the policies for
85        // return
86        let enforced: HashMap<OrganizationId, PolicyDecisionErased> = policy_type
87            .resolve_policy()
88            .get_all_enforced_erased(&policies, &organization_user_policy_contexts)
89            .into_iter()
90            .map(|e| (e.organization_id, e))
91            .collect();
92
93        policies
94            .into_iter()
95            .filter(|p| {
96                p.r#type == policy_type
97                    && match enforced.get(&p.organization_id) {
98                        Some(e) => e.enforced,
99                        None => false,
100                    }
101            })
102            .collect()
103    }
104}
105
106/// Native rust client.
107///
108/// Test-only for now: the strongly-typed enforcement API is not yet exposed to consumers.
109#[cfg(test)]
110impl PolicyClient {
111    /// Evaluate enforcement of the given policy across all organizations,
112    /// returning strongly-typed enforcement results.
113    fn get_all_enforced<P: PolicyDefinition>(
114        &self,
115        policy: P,
116        // TODO: policies and ctx should come from state rather than being specified by the
117        // caller
118        policies: &[Policy],
119        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
120    ) -> Vec<PolicyDecision<P>> {
121        policy.get_all_enforced(policies, organization_user_policy_contexts)
122    }
123
124    /// Evaluate enforcement of the given policy for a single organization,
125    /// returning a strongly-typed enforcement result.
126    fn get_enforced<P: PolicyDefinition>(
127        &self,
128        policy: P,
129        organization_id: OrganizationId,
130        // TODO: policies and ctx should come from state rather than being specified by the
131        // caller
132        policies: &[Policy],
133        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
134    ) -> PolicyDecision<P> {
135        policy.get_enforced(organization_id, policies, organization_user_policy_contexts)
136    }
137}
138
139/// Extension trait that adds a [`policies`](PoliciesClientExt::policies) method to [`Client`].
140pub trait PoliciesClientExt {
141    /// Creates a new [PolicyClient] instance.
142    fn policies(&self) -> PolicyClient;
143}
144
145impl PoliciesClientExt for Client {
146    fn policies(&self) -> PolicyClient {
147        PolicyClient::new()
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use bitwarden_organizations::{OrganizationUserStatusType, OrganizationUserType};
154
155    use super::*;
156    use crate::{
157        MasterPasswordPolicy, MasterPasswordPolicyData, PolicyId, policy_type::PolicyDataType,
158    };
159
160    fn policy(
161        organization_id: OrganizationId,
162        policy_type: PolicyType,
163        data: Option<&str>,
164    ) -> Policy {
165        Policy {
166            id: PolicyId::new_v4(),
167            organization_id,
168            r#type: policy_type,
169            data: data.map(str::to_owned),
170            enabled: true,
171            revision_date: None,
172        }
173    }
174
175    /// A confirmed, non-provider member of `organization_id` that a policy applies to.
176    fn confirmed_member(organization_id: OrganizationId) -> OrganizationUserPolicyContext {
177        OrganizationUserPolicyContext {
178            id: organization_id,
179            status: OrganizationUserStatusType::Confirmed,
180            role: OrganizationUserType::User,
181            enabled: true,
182            use_policies: true,
183            is_provider_user: false,
184        }
185    }
186
187    mod get_enforced {
188        use super::*;
189
190        #[test]
191        fn returns_typed_decision() {
192            let org_id = OrganizationId::new_v4();
193            let views = [policy(
194                org_id,
195                PolicyType::MasterPassword,
196                Some(r#"{"minComplexity":3,"minLength":12}"#),
197            )];
198            let contexts = [confirmed_member(org_id)];
199
200            let result =
201                PolicyClient::new().get_enforced(MasterPasswordPolicy, org_id, &views, &contexts);
202
203            assert_eq!(result.organization_id, org_id);
204            assert!(result.enforced);
205            assert_eq!(
206                result.data,
207                MasterPasswordPolicyData {
208                    min_complexity: Some(3),
209                    min_length: Some(12),
210                    ..Default::default()
211                }
212            );
213        }
214    }
215
216    mod get_all_enforced {
217        use super::*;
218
219        #[test]
220        fn returns_one_decision_per_view() {
221            let org_id = OrganizationId::new_v4();
222            let views = [policy(
223                org_id,
224                PolicyType::MasterPassword,
225                Some(r#"{"minComplexity":3}"#),
226            )];
227            let contexts = [confirmed_member(org_id)];
228
229            let results =
230                PolicyClient::new().get_all_enforced(MasterPasswordPolicy, &views, &contexts);
231
232            assert_eq!(results.len(), 1);
233            assert_eq!(results[0].organization_id, org_id);
234            assert!(results[0].enforced);
235            assert_eq!(results[0].data.min_complexity, Some(3));
236        }
237    }
238
239    mod get_enforced_erased {
240        use super::*;
241
242        #[test]
243        fn returns_erased_decision() {
244            let org_id = OrganizationId::new_v4();
245            let views = vec![policy(
246                org_id,
247                PolicyType::MasterPassword,
248                Some(r#"{"minComplexity":3}"#),
249            )];
250            let contexts = vec![confirmed_member(org_id)];
251
252            let result = PolicyClient::new().get_enforced_erased(
253                PolicyType::MasterPassword,
254                org_id,
255                views,
256                contexts,
257            );
258
259            assert_eq!(result.organization_id, org_id);
260            assert!(result.enforced);
261            assert_eq!(
262                result.data,
263                PolicyDataType::MasterPassword(MasterPasswordPolicyData {
264                    min_complexity: Some(3),
265                    ..Default::default()
266                })
267            );
268        }
269
270        #[test]
271        fn unit_variant_carries_no_data() {
272            let org_id = OrganizationId::new_v4();
273            let views = vec![policy(org_id, PolicyType::SingleOrg, None)];
274            let contexts = vec![confirmed_member(org_id)];
275
276            let result = PolicyClient::new().get_enforced_erased(
277                PolicyType::SingleOrg,
278                org_id,
279                views,
280                contexts,
281            );
282
283            assert!(result.enforced);
284            assert_eq!(result.data, PolicyDataType::SingleOrg);
285        }
286
287        #[test]
288        fn defaults_when_no_matching_view() {
289            let org_id = OrganizationId::new_v4();
290
291            // No view for this org/type: the decision defaults to not-enforced with the
292            // default erased data variant.
293            let result = PolicyClient::new().get_enforced_erased(
294                PolicyType::MasterPassword,
295                org_id,
296                vec![],
297                vec![],
298            );
299
300            assert_eq!(result.organization_id, org_id);
301            assert!(!result.enforced);
302            assert_eq!(
303                result.data,
304                PolicyDataType::MasterPassword(MasterPasswordPolicyData::default())
305            );
306        }
307    }
308
309    mod get_all_enforced_erased {
310        use super::*;
311
312        #[test]
313        fn returns_one_decision_per_view() {
314            let org_id = OrganizationId::new_v4();
315            let views = vec![policy(org_id, PolicyType::MasterPassword, None)];
316            let contexts = vec![confirmed_member(org_id)];
317
318            let results = PolicyClient::new().get_all_enforced_erased(
319                PolicyType::MasterPassword,
320                views,
321                contexts,
322            );
323
324            assert_eq!(results.len(), 1);
325            assert_eq!(results[0].organization_id, org_id);
326            assert!(results[0].enforced);
327            assert_eq!(
328                results[0].data,
329                PolicyDataType::MasterPassword(MasterPasswordPolicyData::default())
330            );
331        }
332
333        #[test]
334        fn evaluates_each_org_independently() {
335            let org_a = OrganizationId::new_v4();
336            let org_b = OrganizationId::new_v4();
337            let views = vec![
338                policy(org_a, PolicyType::MaximumVaultTimeout, None),
339                policy(org_b, PolicyType::MaximumVaultTimeout, None),
340            ];
341            // org_a's member is a subject User; org_b's member is an Owner, who is exempt
342            // from MaximumVaultTimeout.
343            let contexts = vec![
344                confirmed_member(org_a),
345                OrganizationUserPolicyContext {
346                    id: org_b,
347                    status: OrganizationUserStatusType::Confirmed,
348                    role: OrganizationUserType::Owner,
349                    enabled: true,
350                    use_policies: true,
351                    is_provider_user: false,
352                },
353            ];
354
355            let results = PolicyClient::new().get_all_enforced_erased(
356                PolicyType::MaximumVaultTimeout,
357                views,
358                contexts,
359            );
360
361            assert_eq!(results.len(), 2);
362            let a = results
363                .iter()
364                .find(|r| r.organization_id == org_a)
365                .expect("a decision for org_a");
366            let b = results
367                .iter()
368                .find(|r| r.organization_id == org_b)
369                .expect("a decision for org_b");
370            assert!(a.enforced);
371            assert!(!b.enforced);
372        }
373    }
374
375    // Exercised through the public `filter_by_type` (the stable contract), with the real registered
376    // policies, so this characterization survives the implementation refactors that follow.
377    mod filter_by_type {
378        use super::*;
379
380        /// Convenience wrapper around the method under test.
381        fn filter(
382            policies: Vec<Policy>,
383            orgs: Vec<OrganizationUserPolicyContext>,
384            policy_type: PolicyType,
385        ) -> Vec<Policy> {
386            PolicyClient::new().filter_by_type(policies, orgs, policy_type)
387        }
388
389        /// A disabled `Policy` for the gate that drops disabled policies.
390        fn disabled_policy(organization_id: OrganizationId, policy_type: PolicyType) -> Policy {
391            Policy {
392                enabled: false,
393                ..policy(organization_id, policy_type, None)
394            }
395        }
396
397        #[test]
398        fn keeps_a_matching_enabled_policy_and_filters_to_the_requested_type() {
399            let org_id = OrganizationId::new_v4();
400            let policies = vec![
401                policy(org_id, PolicyType::MasterPassword, None),
402                policy(org_id, PolicyType::PasswordGenerator, None),
403            ];
404
405            let result = filter(
406                policies,
407                vec![confirmed_member(org_id)],
408                PolicyType::MasterPassword,
409            );
410
411            assert_eq!(result.len(), 1);
412            assert_eq!(result[0].r#type, PolicyType::MasterPassword);
413        }
414
415        #[test]
416        fn returns_empty_when_no_policy_of_the_requested_type_exists() {
417            let org_id = OrganizationId::new_v4();
418            let policies = vec![policy(org_id, PolicyType::MasterPassword, None)];
419
420            let result = filter(
421                policies,
422                vec![confirmed_member(org_id)],
423                PolicyType::TwoFactorAuthentication,
424            );
425
426            assert!(result.is_empty());
427        }
428
429        #[test]
430        fn drops_a_disabled_policy() {
431            let org_id = OrganizationId::new_v4();
432            let policies = vec![disabled_policy(org_id, PolicyType::MasterPassword)];
433
434            let result = filter(
435                policies,
436                vec![confirmed_member(org_id)],
437                PolicyType::MasterPassword,
438            );
439
440            assert!(result.is_empty());
441        }
442
443        #[test]
444        fn drops_the_policy_when_the_organization_is_disabled() {
445            let org_id = OrganizationId::new_v4();
446            let policies = vec![policy(org_id, PolicyType::MasterPassword, None)];
447            let orgs = vec![OrganizationUserPolicyContext {
448                enabled: false,
449                ..confirmed_member(org_id)
450            }];
451
452            let result = filter(policies, orgs, PolicyType::MasterPassword);
453
454            assert!(result.is_empty());
455        }
456
457        #[test]
458        fn drops_the_policy_when_the_organization_does_not_support_policies() {
459            let org_id = OrganizationId::new_v4();
460            let policies = vec![policy(org_id, PolicyType::MasterPassword, None)];
461            let orgs = vec![OrganizationUserPolicyContext {
462                use_policies: false,
463                ..confirmed_member(org_id)
464            }];
465
466            let result = filter(policies, orgs, PolicyType::MasterPassword);
467
468            assert!(result.is_empty());
469        }
470
471        #[test]
472        fn drops_the_policy_for_a_provider_user() {
473            let org_id = OrganizationId::new_v4();
474            let policies = vec![policy(org_id, PolicyType::MasterPassword, None)];
475            let orgs = vec![OrganizationUserPolicyContext {
476                is_provider_user: true,
477                ..confirmed_member(org_id)
478            }];
479
480            let result = filter(policies, orgs, PolicyType::MasterPassword);
481
482            assert!(result.is_empty());
483        }
484
485        #[test]
486        fn drops_the_policy_for_non_applicable_membership_statuses() {
487            let org_id = OrganizationId::new_v4();
488            for status in [
489                OrganizationUserStatusType::Invited,
490                OrganizationUserStatusType::Revoked,
491                OrganizationUserStatusType::Staged,
492            ] {
493                let label = format!("expected {status:?} to be dropped");
494                let policies = vec![policy(org_id, PolicyType::MasterPassword, None)];
495                let orgs = vec![OrganizationUserPolicyContext {
496                    status,
497                    ..confirmed_member(org_id)
498                }];
499
500                let result = filter(policies, orgs, PolicyType::MasterPassword);
501
502                assert!(result.is_empty(), "{label}");
503            }
504        }
505
506        #[test]
507        fn keeps_the_policy_for_applicable_membership_statuses() {
508            let org_id = OrganizationId::new_v4();
509            for status in [
510                OrganizationUserStatusType::Accepted,
511                OrganizationUserStatusType::Confirmed,
512            ] {
513                let label = format!("expected {status:?} to be kept");
514                let policies = vec![policy(org_id, PolicyType::MasterPassword, None)];
515                let orgs = vec![OrganizationUserPolicyContext {
516                    status,
517                    ..confirmed_member(org_id)
518                }];
519
520                let result = filter(policies, orgs, PolicyType::MasterPassword);
521
522                assert_eq!(result.len(), 1, "{label}");
523            }
524        }
525
526        #[test]
527        fn enforces_the_policy_by_default_when_the_org_is_absent_from_the_contexts() {
528            let org_a = OrganizationId::new_v4();
529            let org_b = OrganizationId::new_v4();
530            let policies = vec![policy(org_a, PolicyType::MasterPassword, None)];
531
532            // Only a context for a different org is provided.
533            let result = filter(
534                policies,
535                vec![confirmed_member(org_b)],
536                PolicyType::MasterPassword,
537            );
538
539            assert_eq!(result.len(), 1);
540        }
541
542        #[test]
543        fn enforces_the_policy_by_default_when_the_contexts_are_empty() {
544            let org_id = OrganizationId::new_v4();
545            let policies = vec![policy(org_id, PolicyType::MasterPassword, None)];
546
547            let result = filter(policies, vec![], PolicyType::MasterPassword);
548
549            assert_eq!(result.len(), 1);
550        }
551
552        #[test]
553        fn applies_master_password_to_an_owner() {
554            // MasterPasswordPolicy has no exempt roles, so it applies even to an Owner.
555            let org_id = OrganizationId::new_v4();
556            let policies = vec![policy(org_id, PolicyType::MasterPassword, None)];
557            let orgs = vec![OrganizationUserPolicyContext {
558                role: OrganizationUserType::Owner,
559                ..confirmed_member(org_id)
560            }];
561
562            let result = filter(policies, orgs, PolicyType::MasterPassword);
563
564            assert_eq!(result.len(), 1);
565        }
566
567        #[test]
568        fn applies_automatic_app_log_in_to_owners_and_admins() {
569            // AutomaticAppLogInPolicy has no exempt roles, so it applies to all members.
570            let org_id = OrganizationId::new_v4();
571            for role in [OrganizationUserType::Owner, OrganizationUserType::Admin] {
572                let label = format!("expected {role:?} to be subject");
573                let policies = vec![policy(org_id, PolicyType::AutomaticAppLogIn, None)];
574                let orgs = vec![OrganizationUserPolicyContext {
575                    role,
576                    ..confirmed_member(org_id)
577                }];
578
579                let result = filter(policies, orgs, PolicyType::AutomaticAppLogIn);
580
581                assert_eq!(result.len(), 1, "{label}");
582            }
583        }
584
585        #[test]
586        fn exempts_an_owner_from_maximum_vault_timeout() {
587            let org_id = OrganizationId::new_v4();
588            let policies = vec![policy(org_id, PolicyType::MaximumVaultTimeout, None)];
589            let orgs = vec![OrganizationUserPolicyContext {
590                role: OrganizationUserType::Owner,
591                ..confirmed_member(org_id)
592            }];
593
594            let result = filter(policies, orgs, PolicyType::MaximumVaultTimeout);
595
596            assert!(result.is_empty());
597        }
598
599        #[test]
600        fn applies_maximum_vault_timeout_to_admins_and_users() {
601            let org_id = OrganizationId::new_v4();
602            for role in [OrganizationUserType::Admin, OrganizationUserType::User] {
603                let label = format!("expected {role:?} to be subject");
604                let policies = vec![policy(org_id, PolicyType::MaximumVaultTimeout, None)];
605                let orgs = vec![OrganizationUserPolicyContext {
606                    role,
607                    ..confirmed_member(org_id)
608                }];
609
610                let result = filter(policies, orgs, PolicyType::MaximumVaultTimeout);
611
612                assert_eq!(result.len(), 1, "{label}");
613            }
614        }
615
616        #[test]
617        fn two_factor_authentication_exempts_owners_and_admins_via_default_impl() {
618            let org_id = OrganizationId::new_v4();
619            for role in [OrganizationUserType::Owner, OrganizationUserType::Admin] {
620                let label = format!("expected {role:?} to be exempt");
621                let policies = vec![policy(org_id, PolicyType::TwoFactorAuthentication, None)];
622                let orgs = vec![OrganizationUserPolicyContext {
623                    role,
624                    ..confirmed_member(org_id)
625                }];
626
627                let result = filter(policies, orgs, PolicyType::TwoFactorAuthentication);
628
629                assert!(result.is_empty(), "{label}");
630            }
631        }
632
633        #[test]
634        fn two_factor_authentication_applies_to_a_regular_user_via_default_impl() {
635            let org_id = OrganizationId::new_v4();
636            let policies = vec![policy(org_id, PolicyType::TwoFactorAuthentication, None)];
637
638            let result = filter(
639                policies,
640                vec![confirmed_member(org_id)],
641                PolicyType::TwoFactorAuthentication,
642            );
643
644            assert_eq!(result.len(), 1);
645        }
646
647        #[test]
648        fn filters_independently_across_multiple_organizations() {
649            // org_a's member is a subject User; org_b's member is an Owner, exempt from
650            // MaximumVaultTimeout.
651            let org_a = OrganizationId::new_v4();
652            let org_b = OrganizationId::new_v4();
653            let policies = vec![
654                policy(org_a, PolicyType::MaximumVaultTimeout, None),
655                policy(org_b, PolicyType::MaximumVaultTimeout, None),
656            ];
657            let orgs = vec![
658                confirmed_member(org_a),
659                OrganizationUserPolicyContext {
660                    role: OrganizationUserType::Owner,
661                    ..confirmed_member(org_b)
662                },
663            ];
664
665            let result = filter(policies, orgs, PolicyType::MaximumVaultTimeout);
666
667            assert_eq!(result.len(), 1);
668            assert_eq!(result[0].organization_id, org_a);
669        }
670    }
671}