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