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