Skip to main content

bitwarden_policies/
policy_sync_handler.rs

1use std::sync::Arc;
2
3use bitwarden_core::{FromClient, require};
4use bitwarden_state::repository::{Repository, RepositoryOption};
5use bitwarden_sync::{SyncHandler, SyncHandlerError};
6
7use crate::{Policy, PolicyId};
8
9/// Sync handler for policies
10///
11/// This handler persists organization policies to SDK-managed storage.
12#[derive(FromClient)]
13pub struct PolicySyncHandler {
14    repository: Option<Arc<dyn Repository<Policy>>>,
15}
16
17#[async_trait::async_trait]
18impl SyncHandler for PolicySyncHandler {
19    async fn on_sync(
20        &self,
21        response: &bitwarden_api_api::models::SyncResponseModel,
22    ) -> Result<(), SyncHandlerError> {
23        let repository = self.repository.require()?;
24        // Prefer `policies_new` (policies for orgs where the user is confirmed/accepted),
25        // falling back to the legacy `policies` (confirmed only) for older servers.
26        let api_policies = require!(
27            response
28                .policies_new
29                .as_ref()
30                .or(response.policies.as_ref())
31        );
32
33        let policies: Vec<(PolicyId, Policy)> = api_policies
34            .iter()
35            .filter_map(|p| {
36                Policy::try_from(p.clone())
37                    .inspect_err(
38                        |e| tracing::error!(id = ?p.id, error = ?e, "Failed to deserialize policy"),
39                    )
40                    .ok()
41                    .map(|policy| (policy.id, policy))
42            })
43            .collect();
44
45        repository.replace_all(policies).await?;
46
47        Ok(())
48    }
49}
50
51#[cfg(test)]
52mod tests {
53    use std::sync::Arc;
54
55    use bitwarden_api_api::models::{PolicyResponseModel, PolicyType, SyncResponseModel};
56    use bitwarden_test::MemoryRepository;
57
58    use super::*;
59
60    fn make_policy_response(id: uuid::Uuid, r#type: PolicyType) -> PolicyResponseModel {
61        PolicyResponseModel {
62            object: Some("policy".to_string()),
63            id: Some(id),
64            organization_id: Some(uuid::Uuid::new_v4()),
65            r#type: Some(r#type),
66            data: None,
67            enabled: Some(true),
68            revision_date: Some("2025-01-01T00:00:00Z".to_string()),
69        }
70    }
71
72    #[tokio::test]
73    async fn test_on_sync_replaces_existing_policies() {
74        let repository = Arc::new(MemoryRepository::<Policy>::default());
75        let handler = PolicySyncHandler {
76            repository: Some(repository.clone()),
77        };
78
79        // First sync with two policies
80        let id1 = uuid::Uuid::new_v4();
81        let id2 = uuid::Uuid::new_v4();
82        let response = SyncResponseModel {
83            policies_new: Some(vec![
84                make_policy_response(id1, PolicyType::MasterPassword),
85                make_policy_response(id2, PolicyType::SingleOrg),
86            ]),
87            ..Default::default()
88        };
89        handler.on_sync(&response).await.unwrap();
90        assert_eq!(repository.list().await.unwrap().len(), 2);
91
92        // Second sync with only one policy — old ones should be gone
93        let id3 = uuid::Uuid::new_v4();
94        let response = SyncResponseModel {
95            policies_new: Some(vec![make_policy_response(id3, PolicyType::RequireSso)]),
96            ..Default::default()
97        };
98        handler.on_sync(&response).await.unwrap();
99
100        let stored = repository.list().await.unwrap();
101        assert_eq!(stored.len(), 1);
102        assert!(repository.get(PolicyId::new(id1)).await.unwrap().is_none());
103        assert!(repository.get(PolicyId::new(id2)).await.unwrap().is_none());
104        assert!(repository.get(PolicyId::new(id3)).await.unwrap().is_some());
105    }
106
107    #[tokio::test]
108    async fn test_on_sync_falls_back_to_legacy_policies() {
109        let repository = Arc::new(MemoryRepository::<Policy>::default());
110        let handler = PolicySyncHandler {
111            repository: Some(repository.clone()),
112        };
113
114        let id = uuid::Uuid::new_v4();
115        let response = SyncResponseModel {
116            policies: Some(vec![make_policy_response(id, PolicyType::MasterPassword)]),
117            ..Default::default()
118        };
119        handler.on_sync(&response).await.unwrap();
120
121        assert!(repository.get(PolicyId::new(id)).await.unwrap().is_some());
122    }
123
124    #[tokio::test]
125    async fn test_on_sync_skips_unknown_policy_type() {
126        let repository = Arc::new(MemoryRepository::<Policy>::default());
127        let handler = PolicySyncHandler {
128            repository: Some(repository.clone()),
129        };
130
131        let known_id = uuid::Uuid::new_v4();
132        let unknown_id = uuid::Uuid::new_v4();
133        let response = SyncResponseModel {
134            policies_new: Some(vec![
135                make_policy_response(known_id, PolicyType::MasterPassword),
136                make_policy_response(unknown_id, PolicyType::__Unknown(9999)),
137            ]),
138            ..Default::default()
139        };
140        handler.on_sync(&response).await.unwrap();
141
142        // The unknown-typed policy is dropped; the valid one persists.
143        let stored = repository.list().await.unwrap();
144        assert_eq!(stored.len(), 1);
145        assert!(
146            repository
147                .get(PolicyId::new(known_id))
148                .await
149                .unwrap()
150                .is_some()
151        );
152        assert!(
153            repository
154                .get(PolicyId::new(unknown_id))
155                .await
156                .unwrap()
157                .is_none()
158        );
159    }
160
161    #[tokio::test]
162    async fn test_on_sync_no_policies_returns_error() {
163        let repository = Arc::new(MemoryRepository::<Policy>::default());
164        let handler = PolicySyncHandler {
165            repository: Some(repository.clone()),
166        };
167
168        let response = SyncResponseModel::default();
169        let result = handler.on_sync(&response).await;
170        assert!(result.is_err());
171    }
172}