Skip to main content

bitwarden_policies/
filter.rs

1#![allow(dead_code)]
2
3//! Policy filtering logic.
4//!
5//! Provides the [`Policy`] trait for determining which policies
6//! should be enforced against the current user based on business rules.
7
8use std::collections::HashMap;
9
10use bitwarden_organizations::{OrganizationUserStatusType, OrganizationUserType};
11use uuid::Uuid;
12
13use crate::{
14    models::{OrganizationUserPolicyContext, PolicyView},
15    policy_type::PolicyType,
16};
17
18/// Defines the filtering behavior for a specific policy type.
19///
20/// Implement this trait to control how a policy is enforced.
21pub trait Policy: Send + Sync + 'static {
22    /// Returns the policy type this definition handles.
23    fn policy_type(&self) -> PolicyType;
24
25    /// Returns the organization roles that are exempt from this policy.
26    ///
27    /// Defaults to [`Owner`](OrganizationUserType::Owner) and
28    /// [`Admin`](OrganizationUserType::Admin).
29    fn exempt_roles(&self) -> &[OrganizationUserType] {
30        &[OrganizationUserType::Owner, OrganizationUserType::Admin]
31    }
32
33    /// Returns whether provider users are exempt from this policy.
34    ///
35    /// Defaults to `true`.
36    fn exempt_providers(&self) -> bool {
37        true
38    }
39
40    /// Returns the organization membership statuses for which this policy applies.
41    ///
42    /// Defaults to [`Accepted`](OrganizationUserStatusType::Accepted) and
43    /// [`Confirmed`](OrganizationUserStatusType::Confirmed).
44    fn applicable_statuses(&self) -> &[OrganizationUserStatusType] {
45        &[
46            OrganizationUserStatusType::Accepted,
47            OrganizationUserStatusType::Confirmed,
48        ]
49    }
50}
51
52/// Extension trait that adds a [`filter`](PolicyFilter::filter) method to every [`Policy`].
53///
54/// Implemented automatically for all `T: Policy`.
55pub trait PolicyFilter: Policy {
56    /// Filters `policies` to those that should be enforced against the user.
57    /// This evaluates common business rules (e.g. the policy is enabled),
58    /// as well as policy-specific rules according to its [`Policy`].
59    ///
60    /// If a policy's organization is not present in `organization_user_policy_contexts`, the policy
61    /// is enforced by default.
62    fn filter<'a>(
63        &self,
64        policies: &'a [PolicyView],
65        organization_user_policy_contexts: &[OrganizationUserPolicyContext],
66    ) -> Vec<&'a PolicyView> {
67        let org_map: HashMap<&Uuid, &OrganizationUserPolicyContext> =
68            organization_user_policy_contexts
69                .iter()
70                .map(|o| (&o.id, o))
71                .collect();
72
73        policies
74            .iter()
75            .filter(|p| p.r#type == self.policy_type())
76            .filter(|p| p.enabled)
77            .filter(|p| {
78                match org_map.get(&p.organization_id) {
79                    Some(org) => {
80                        org.enabled
81                            && org.use_policies
82                            && self.applicable_statuses().contains(&org.status)
83                            && !self.exempt_roles().contains(&org.role)
84                            && !(org.is_provider_user && self.exempt_providers())
85                    }
86                    None => true, // Unknown org: enforce by default
87                }
88            })
89            .collect()
90    }
91}
92
93impl<T: Policy> PolicyFilter for T {}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    fn policy_view(organization_id: Uuid, policy_type: PolicyType, enabled: bool) -> PolicyView {
100        PolicyView {
101            id: Uuid::new_v4(),
102            organization_id,
103            r#type: policy_type,
104            data: None,
105            enabled,
106            revision_date: Default::default(),
107        }
108    }
109
110    fn organization(
111        id: Uuid,
112        user_type: OrganizationUserType,
113        status: OrganizationUserStatusType,
114        provider: bool,
115    ) -> OrganizationUserPolicyContext {
116        OrganizationUserPolicyContext {
117            id,
118            role: user_type,
119            status,
120            enabled: true,
121            use_policies: true,
122            is_provider_user: provider,
123        }
124    }
125
126    struct TestPolicy;
127    impl Policy for TestPolicy {
128        fn policy_type(&self) -> PolicyType {
129            PolicyType::MasterPassword
130        }
131
132        // These happen to match the default impl, but repeating here
133        // to decouple the filter tests from the default impl
134        fn exempt_roles(&self) -> &[OrganizationUserType] {
135            &[OrganizationUserType::Owner, OrganizationUserType::Admin]
136        }
137
138        fn exempt_providers(&self) -> bool {
139            true
140        }
141
142        fn applicable_statuses(&self) -> &[OrganizationUserStatusType] {
143            &[
144                OrganizationUserStatusType::Accepted,
145                OrganizationUserStatusType::Confirmed,
146            ]
147        }
148    }
149
150    #[test]
151    fn matching_policy_is_returned() {
152        let org_id = Uuid::new_v4();
153        let policies = [policy_view(org_id, PolicyType::MasterPassword, true)];
154        let orgs = [organization(
155            org_id,
156            OrganizationUserType::User,
157            OrganizationUserStatusType::Confirmed,
158            false,
159        )];
160
161        let result = TestPolicy.filter(&policies, &orgs);
162        assert_eq!(result.len(), 1);
163    }
164
165    #[test]
166    fn disabled_organization_is_filtered_out() {
167        let org_id = Uuid::new_v4();
168        let orgs = [OrganizationUserPolicyContext {
169            enabled: false,
170            id: org_id,
171            role: OrganizationUserType::User,
172            status: OrganizationUserStatusType::Confirmed,
173            use_policies: true,
174            is_provider_user: false,
175        }];
176        let policies = [policy_view(org_id, PolicyType::MasterPassword, true)];
177
178        let result = TestPolicy.filter(&policies, &orgs);
179        assert!(result.is_empty());
180    }
181
182    #[test]
183    fn disabled_policy_is_filtered_out() {
184        let org_id = Uuid::new_v4();
185        let policies = [policy_view(org_id, PolicyType::MasterPassword, false)];
186        let orgs = [organization(
187            org_id,
188            OrganizationUserType::User,
189            OrganizationUserStatusType::Confirmed,
190            false,
191        )];
192
193        let result = TestPolicy.filter(&policies, &orgs);
194        assert!(result.is_empty());
195    }
196
197    #[test]
198    fn wrong_policy_type_is_filtered_out() {
199        let org_id = Uuid::new_v4();
200        let policies = [policy_view(org_id, PolicyType::PasswordGenerator, true)];
201        let orgs = [organization(
202            org_id,
203            OrganizationUserType::User,
204            OrganizationUserStatusType::Confirmed,
205            false,
206        )];
207
208        let result = TestPolicy.filter(&policies, &orgs);
209        assert!(result.is_empty());
210    }
211
212    #[test]
213    fn use_policies_false_is_filtered_out() {
214        let org_id = Uuid::new_v4();
215        let orgs = [OrganizationUserPolicyContext {
216            id: org_id,
217            role: OrganizationUserType::User,
218            status: OrganizationUserStatusType::Confirmed,
219            enabled: true,
220            use_policies: false,
221            is_provider_user: false,
222        }];
223        let policies = [policy_view(org_id, PolicyType::MasterPassword, true)];
224
225        let result = TestPolicy.filter(&policies, &orgs);
226        assert!(result.is_empty());
227    }
228
229    #[test]
230    fn exempt_role_is_filtered_out() {
231        let org_id = Uuid::new_v4();
232        let policies = [policy_view(org_id, PolicyType::MasterPassword, true)];
233        let orgs = [organization(
234            org_id,
235            OrganizationUserType::Owner,
236            OrganizationUserStatusType::Confirmed,
237            false,
238        )];
239
240        let result = TestPolicy.filter(&policies, &orgs);
241        assert!(result.is_empty());
242    }
243
244    #[test]
245    fn non_applicable_status_is_filtered_out() {
246        let org_id = Uuid::new_v4();
247        let policies = [policy_view(org_id, PolicyType::MasterPassword, true)];
248        let orgs = [organization(
249            org_id,
250            OrganizationUserType::User,
251            OrganizationUserStatusType::Revoked,
252            false,
253        )];
254
255        let result = TestPolicy.filter(&policies, &orgs);
256        assert!(result.is_empty());
257    }
258
259    #[test]
260    fn provider_is_filtered_out() {
261        let org_id = Uuid::new_v4();
262        let policies = [policy_view(org_id, PolicyType::MasterPassword, true)];
263        let orgs = [organization(
264            org_id,
265            OrganizationUserType::User,
266            OrganizationUserStatusType::Confirmed,
267            true,
268        )];
269
270        let result = TestPolicy.filter(&policies, &orgs);
271        assert!(result.is_empty());
272    }
273
274    #[test]
275    fn missing_org_enforces_by_default() {
276        let policies = [policy_view(
277            Uuid::new_v4(),
278            PolicyType::MasterPassword,
279            true,
280        )];
281
282        let result = TestPolicy.filter(&policies, &[]);
283        assert_eq!(result.len(), 1);
284    }
285}