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