bitwarden_managed_settings/
managed_settings_client.rs1use std::sync::{Arc, RwLock};
2
3use bitwarden_core::Client;
4use bitwarden_managed_settings_types::ManagementProfile;
5
6#[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
25impl ManagedSettingsClient {
27 pub(crate) fn from_profile(profile: Arc<RwLock<Option<ManagementProfile>>>) -> Self {
28 Self { profile }
29 }
30
31 pub fn cell(&self) -> Arc<RwLock<Option<ManagementProfile>>> {
35 self.profile.clone()
36 }
37
38 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 #[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 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 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 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
93pub trait ManagedSettingsClientExt {
95 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}