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")]
26pub struct ManagementProfile {
27    /// Schema version. Bumped when the dotted-key namespace changes incompatibly.
28    pub version: u32,
29    /// Unix timestamp (seconds) at which the host last refreshed the profile.
30    ///
31    /// This field participates in equality, so two profiles carrying identical `settings` compare
32    /// unequal when the host re-read the source and re-stamped the timestamp. Do not use `==` to
33    /// detect whether the managed settings themselves changed; compare `settings` instead.
34    pub updated_at: i64,
35    /// Dotted key to JSON-encoded value string.
36    pub settings: HashMap<String, String>,
37}
38
39impl ManagementProfile {
40    /// An empty profile, equivalent to "no admin overrides". Every `is_managed` returns `false`.
41    pub fn empty() -> Self {
42        Self {
43            version: 1,
44            updated_at: 0,
45            settings: HashMap::new(),
46        }
47    }
48
49    /// Returns `true` if `key` is present. Presence implies the value is forced.
50    pub fn is_managed(&self, key: &str) -> bool {
51        self.settings.contains_key(key)
52    }
53
54    /// Returns the raw JSON-encoded string stored under `key`, if any.
55    pub fn get(&self, key: &str) -> Option<String> {
56        self.settings.get(key).cloned()
57    }
58
59    /// Get and JSON-decode a value to `T`.
60    pub fn get_as<T: serde::de::DeserializeOwned>(
61        &self,
62        key: &str,
63    ) -> Result<Option<T>, ManagedSettingsError> {
64        match self.settings.get(key) {
65            None => Ok(None),
66            Some(raw) => serde_json::from_str(raw)
67                .map(Some)
68                .map_err(|e| ManagedSettingsError::Decode(e.to_string())),
69        }
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    fn profile_with(entries: &[(&str, &str)]) -> ManagementProfile {
78        ManagementProfile {
79            version: 1,
80            updated_at: 1_750_000_000,
81            settings: entries
82                .iter()
83                .map(|(k, v)| (k.to_string(), v.to_string()))
84                .collect(),
85        }
86    }
87
88    #[test]
89    fn is_managed_reflects_key_presence() {
90        let profile = profile_with(&[("environment.base", "\"https://vault.example.com\"")]);
91
92        assert!(profile.is_managed("environment.base"));
93        assert!(!profile.is_managed("environment.api"));
94    }
95
96    #[test]
97    fn get_returns_the_raw_json_encoded_value() {
98        let profile = profile_with(&[("environment.base", "\"https://vault.example.com\"")]);
99
100        assert_eq!(
101            profile.get("environment.base"),
102            Some("\"https://vault.example.com\"".to_string())
103        );
104    }
105
106    #[test]
107    fn get_returns_none_for_an_absent_key() {
108        let profile = profile_with(&[("environment.base", "\"https://vault.example.com\"")]);
109
110        assert_eq!(profile.get("environment.api"), None);
111    }
112
113    #[test]
114    fn get_as_decodes_a_present_value() {
115        let profile = profile_with(&[("generator.password.length", "14")]);
116
117        assert_eq!(
118            profile.get_as::<u32>("generator.password.length").unwrap(),
119            Some(14)
120        );
121    }
122
123    #[test]
124    fn get_as_returns_none_for_an_absent_key() {
125        let profile = profile_with(&[("generator.password.length", "14")]);
126
127        assert_eq!(
128            profile
129                .get_as::<u32>("generator.password.uppercase")
130                .unwrap(),
131            None
132        );
133    }
134
135    #[test]
136    fn get_as_errors_when_the_value_does_not_match_the_requested_type() {
137        let profile = profile_with(&[("generator.password.length", "\"fourteen\"")]);
138
139        let error = profile
140            .get_as::<u32>("generator.password.length")
141            .expect_err("decoding a string as u32 should fail");
142
143        assert!(matches!(error, ManagedSettingsError::Decode(_)));
144    }
145
146    #[test]
147    fn empty_profile_manages_nothing() {
148        let profile = ManagementProfile::empty();
149
150        assert!(!profile.is_managed("environment.base"));
151        assert_eq!(profile.get("environment.base"), None);
152        assert_eq!(profile.get_as::<u32>("environment.base").unwrap(), None);
153    }
154}