bitwarden_managed_settings/
managed_settings_client.rs1use 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#[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
27impl ManagedSettingsClient {
29 pub(crate) fn from_profile(profile: Arc<RwLock<Option<ManagementProfile>>>) -> Self {
30 Self { profile }
31 }
32
33 pub fn cell(&self) -> Arc<RwLock<Option<ManagementProfile>>> {
37 self.profile.clone()
38 }
39
40 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 #[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 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 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 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
95pub trait ManagedSettingsClientExt {
97 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}