Skip to main content

bitwarden_managed_settings/
managed_settings_client.rs

1use std::sync::{Arc, RwLock};
2
3use bitwarden_core::Client;
4use bitwarden_managed_settings_types::ManagementProfile;
5#[cfg(feature = "wasm")]
6use wasm_bindgen::prelude::*;
7
8/// Handle to the host system's Unified Endpoint Management profile.
9///
10/// The host application constructs one of these at boot and pushes profiles into it, and hands its
11/// [`cell`](ManagedSettingsClient::cell) to
12/// [`bitwarden_core::ClientBuilder::with_managed_profile`] so the SDK reads the same profile.
13/// Clones share the underlying profile, so an update pushed through one clone is observed by all of
14/// them.
15#[derive(Clone)]
16#[cfg_attr(feature = "wasm", wasm_bindgen)]
17pub struct ManagedSettingsClient {
18    profile: Arc<RwLock<Option<ManagementProfile>>>,
19}
20
21impl Default for ManagedSettingsClient {
22    fn default() -> Self {
23        Self::new()
24    }
25}
26
27/// Methods whose signatures cannot cross an FFI boundary, so they stay off the binding surface.
28impl ManagedSettingsClient {
29    pub(crate) fn from_profile(profile: Arc<RwLock<Option<ManagementProfile>>>) -> Self {
30        Self { profile }
31    }
32
33    /// The shared profile cell, for handing to
34    /// [`bitwarden_core::ClientBuilder::with_managed_profile`] so a constructed SDK client reads
35    /// the same profile the host pushes into this handle.
36    pub fn cell(&self) -> Arc<RwLock<Option<ManagementProfile>>> {
37        self.profile.clone()
38    }
39
40    /// The active profile, or `None` when the host has not pushed one.
41    pub fn current_profile(&self) -> Option<ManagementProfile> {
42        self.profile
43            .read()
44            .expect("managed-settings cell poisoned")
45            .clone()
46    }
47}
48
49#[cfg_attr(feature = "wasm", wasm_bindgen)]
50impl ManagedSettingsClient {
51    /// Fresh handle with no active profile. The host should call this once at boot.
52    #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
53    pub fn new() -> Self {
54        Self {
55            profile: Arc::new(RwLock::new(None)),
56        }
57    }
58
59    /// Replace the active profile. Clear the profile with `None`.
60    pub fn update_profile(&self, profile: Option<ManagementProfile>) {
61        match &profile {
62            Some(p) => tracing::info!(
63                version = p.version,
64                keys = p.settings.len(),
65                "Managed settings profile updated"
66            ),
67            None => tracing::info!("Managed settings profile cleared"),
68        }
69
70        *self
71            .profile
72            .write()
73            .expect("managed-settings cell poisoned") = profile;
74    }
75
76    /// Returns `true` if `key` is present in the active profile.
77    pub fn is_managed(&self, key: String) -> bool {
78        self.profile
79            .read()
80            .expect("managed-settings cell poisoned")
81            .as_ref()
82            .is_some_and(|p| p.is_managed(&key))
83    }
84
85    /// Raw JSON-encoded value for `key`, if a value is present.
86    pub fn get(&self, key: String) -> Option<String> {
87        self.profile
88            .read()
89            .expect("managed-settings cell poisoned")
90            .as_ref()
91            .and_then(|p| p.get(&key))
92    }
93}
94
95/// Read the UEM profile handle back from a constructed [`bitwarden_core::Client`].
96pub trait ManagedSettingsClientExt {
97    /// Administrator-enforced settings operations.
98    fn managed_settings(&self) -> ManagedSettingsClient;
99}
100
101impl ManagedSettingsClientExt for Client {
102    fn managed_settings(&self) -> ManagedSettingsClient {
103        ManagedSettingsClient::from_profile(self.internal.managed_profile_handle())
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use std::collections::HashMap;
110
111    use bitwarden_core::ClientBuilder;
112
113    use super::*;
114
115    fn profile_with(key: &str, json_value: &str) -> ManagementProfile {
116        ManagementProfile {
117            version: 1,
118            updated_at: 1_750_000_000,
119            settings: HashMap::from([(key.to_string(), json_value.to_string())]),
120        }
121    }
122
123    #[test]
124    fn a_new_client_manages_nothing() {
125        let client = ManagedSettingsClient::new();
126
127        assert_eq!(client.get("environment.base".to_string()), None);
128        assert!(!client.is_managed("environment.base".to_string()));
129        assert_eq!(client.current_profile(), None);
130    }
131
132    #[test]
133    fn get_reflects_an_updated_profile() {
134        let client = ManagedSettingsClient::new();
135        let profile = profile_with("environment.base", "\"https://vault.example.com\"");
136
137        client.update_profile(Some(profile.clone()));
138
139        assert_eq!(
140            client.get("environment.base".to_string()),
141            Some("\"https://vault.example.com\"".to_string())
142        );
143        assert!(client.is_managed("environment.base".to_string()));
144        assert_eq!(client.current_profile(), Some(profile));
145    }
146
147    #[test]
148    fn updating_with_none_clears_the_profile() {
149        let client = ManagedSettingsClient::new();
150        client.update_profile(Some(profile_with("environment.base", "\"https://a\"")));
151
152        client.update_profile(None);
153
154        assert_eq!(client.get("environment.base".to_string()), None);
155        assert!(!client.is_managed("environment.base".to_string()));
156        assert_eq!(client.current_profile(), None);
157    }
158
159    #[test]
160    fn a_clone_observes_an_update_made_on_the_original() {
161        let client = ManagedSettingsClient::new();
162        let clone = client.clone();
163        let profile = profile_with("environment.base", "\"https://vault.example.com\"");
164
165        client.update_profile(Some(profile.clone()));
166
167        assert_eq!(
168            clone.get("environment.base".to_string()),
169            Some("\"https://vault.example.com\"".to_string())
170        );
171        assert_eq!(clone.current_profile(), Some(profile));
172    }
173
174    #[test]
175    fn the_shared_cell_observes_an_update_made_through_the_handle() {
176        let client = ManagedSettingsClient::new();
177        let cell = client.cell();
178        let profile = profile_with("environment.base", "\"https://vault.example.com\"");
179
180        client.update_profile(Some(profile.clone()));
181
182        assert_eq!(
183            *cell.read().expect("managed-settings cell poisoned"),
184            Some(profile)
185        );
186    }
187
188    #[test]
189    fn a_client_built_with_the_cell_reads_the_pushed_profile() {
190        let host_handle = ManagedSettingsClient::new();
191        let client = ClientBuilder::new()
192            .with_managed_profile(host_handle.cell())
193            .build();
194
195        host_handle.update_profile(Some(profile_with(
196            "environment.base",
197            "\"https://vault.example.com\"",
198        )));
199
200        assert_eq!(
201            client
202                .managed_settings()
203                .get("environment.base".to_string()),
204            Some("\"https://vault.example.com\"".to_string())
205        );
206    }
207
208    #[test]
209    fn a_client_built_without_a_cell_manages_nothing() {
210        let client = ClientBuilder::new().build();
211
212        assert_eq!(
213            client
214                .managed_settings()
215                .get("environment.base".to_string()),
216            None
217        );
218        assert_eq!(client.managed_settings().current_profile(), None);
219    }
220}