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