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