Skip to main content

bitwarden_managed_settings_types/
profile.rs

1use std::collections::HashMap;
2
3use bitwarden_error::bitwarden_error;
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use tsify::Tsify;
8
9/// Errors that can occur while reading a [`ManagementProfile`].
10#[bitwarden_error(flat)]
11#[derive(Debug, Error)]
12pub enum ManagedSettingsError {
13    /// The value stored under the requested key could not be parsed as the expected shape.
14    #[error("Failed to decode managed settings value: {0}")]
15    Decode(String),
16}
17
18/// A point-in-time snapshot of administrator-forced configuration for this client.
19///
20/// `settings` maps dotted keys (e.g. `"generator.password.length"`) to JSON-encoded
21/// strings. A plain `String` is used (not `serde_json::Value`) because it has a UniFFI
22/// representation. Callers parse on demand through [`get_as`](ManagementProfile::get_as).
23///
24/// This is client configuration forced by an operating system's Unified Endpoint Management
25/// (UEM/MDM) channel. It is not Vault Data and involves no cryptography.
26#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
27#[serde(rename_all = "camelCase")]
28#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
29#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
30pub struct ManagementProfile {
31    /// Schema version. Bumped when the dotted-key namespace changes incompatibly.
32    pub version: u32,
33    /// Unix timestamp (seconds) at which the host last refreshed the profile.
34    ///
35    /// This field participates in equality, so two profiles carrying identical `settings` compare
36    /// unequal when the host re-read the source and re-stamped the timestamp. Do not use `==` to
37    /// detect whether the managed settings themselves changed; compare `settings` instead.
38    pub updated_at: i64,
39    /// Dotted key to JSON-encoded value string.
40    pub settings: HashMap<String, String>,
41}
42
43impl ManagementProfile {
44    /// An empty profile, equivalent to "no admin overrides". Every `is_managed` returns `false`.
45    pub fn empty() -> Self {
46        Self {
47            version: 1,
48            updated_at: 0,
49            settings: HashMap::new(),
50        }
51    }
52
53    /// Returns `true` if `key` is present. Presence implies the value is forced.
54    pub fn is_managed(&self, key: &str) -> bool {
55        self.settings.contains_key(key)
56    }
57
58    /// Returns the raw JSON-encoded string stored under `key`, if any.
59    pub fn get(&self, key: &str) -> Option<String> {
60        self.settings.get(key).cloned()
61    }
62
63    /// Get and JSON-decode a value to `T`.
64    pub fn get_as<T: serde::de::DeserializeOwned>(
65        &self,
66        key: &str,
67    ) -> Result<Option<T>, ManagedSettingsError> {
68        match self.settings.get(key) {
69            None => Ok(None),
70            Some(raw) => serde_json::from_str(raw)
71                .map(Some)
72                .map_err(|e| ManagedSettingsError::Decode(e.to_string())),
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    fn profile_with(entries: &[(&str, &str)]) -> ManagementProfile {
82        ManagementProfile {
83            version: 1,
84            updated_at: 1_750_000_000,
85            settings: entries
86                .iter()
87                .map(|(k, v)| (k.to_string(), v.to_string()))
88                .collect(),
89        }
90    }
91
92    #[test]
93    fn is_managed_reflects_key_presence() {
94        let profile = profile_with(&[("environment.base", "\"https://vault.example.com\"")]);
95
96        assert!(profile.is_managed("environment.base"));
97        assert!(!profile.is_managed("environment.api"));
98    }
99
100    #[test]
101    fn get_returns_the_raw_json_encoded_value() {
102        let profile = profile_with(&[("environment.base", "\"https://vault.example.com\"")]);
103
104        assert_eq!(
105            profile.get("environment.base"),
106            Some("\"https://vault.example.com\"".to_string())
107        );
108    }
109
110    #[test]
111    fn get_returns_none_for_an_absent_key() {
112        let profile = profile_with(&[("environment.base", "\"https://vault.example.com\"")]);
113
114        assert_eq!(profile.get("environment.api"), None);
115    }
116
117    #[test]
118    fn get_as_decodes_a_present_value() {
119        let profile = profile_with(&[("generator.password.length", "14")]);
120
121        assert_eq!(
122            profile.get_as::<u32>("generator.password.length").unwrap(),
123            Some(14)
124        );
125    }
126
127    #[test]
128    fn get_as_returns_none_for_an_absent_key() {
129        let profile = profile_with(&[("generator.password.length", "14")]);
130
131        assert_eq!(
132            profile
133                .get_as::<u32>("generator.password.uppercase")
134                .unwrap(),
135            None
136        );
137    }
138
139    #[test]
140    fn get_as_errors_when_the_value_does_not_match_the_requested_type() {
141        let profile = profile_with(&[("generator.password.length", "\"fourteen\"")]);
142
143        let error = profile
144            .get_as::<u32>("generator.password.length")
145            .expect_err("decoding a string as u32 should fail");
146
147        assert!(matches!(error, ManagedSettingsError::Decode(_)));
148    }
149
150    #[test]
151    fn empty_profile_manages_nothing() {
152        let profile = ManagementProfile::empty();
153
154        assert!(!profile.is_managed("environment.base"));
155        assert_eq!(profile.get("environment.base"), None);
156        assert_eq!(profile.get_as::<u32>("environment.base").unwrap(), None);
157    }
158}