Skip to main content

bitwarden_policies/
policy.rs

1//! The [`Policy`] record: the raw, persisted representation of an organization policy.
2
3use bitwarden_core::{MissingFieldError, OrganizationId, require};
4use bitwarden_uuid::uuid_newtype;
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8use crate::policy_type::PolicyType;
9
10uuid_newtype!(pub PolicyId);
11
12/// An organization policy in the raw data format that is sent over the FFI.
13///
14/// This is the storage-layer record. It is resolved into a strongly-typed,
15/// per-policy projection at the enforcement boundary.
16#[derive(Serialize, Deserialize, Debug, Clone)]
17#[serde(rename_all = "camelCase")]
18#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
19#[bitwarden_ffi::wasm_record]
20pub struct Policy {
21    /// The policy's unique ID.
22    pub id: PolicyId,
23    /// The organization this policy belongs to.
24    pub organization_id: OrganizationId,
25    /// The type of policy.
26    pub r#type: PolicyType,
27    /// The policy's additional configuration data as a JSON string, if any.
28    pub data: Option<String>,
29    /// Whether the policy is enabled.
30    pub enabled: bool,
31    /// When the policy was last modified.
32    pub revision_date: Option<DateTime<Utc>>,
33}
34
35bitwarden_state::register_repository_item!(PolicyId => Policy, "Policy");
36
37/// Errors that can occur when parsing a [`Policy`] from its raw API representation.
38#[derive(Debug, thiserror::Error)]
39pub enum PolicyParseError {
40    /// A required field was missing from the API response.
41    #[error(transparent)]
42    MissingField(#[from] MissingFieldError),
43    /// The server returned a policy type this SDK version does not recognize.
44    #[error("Unknown policy type: {0}")]
45    UnknownPolicyType(i64),
46    /// The revision date could not be parsed.
47    #[error(transparent)]
48    InvalidRevisionDate(#[from] chrono::ParseError),
49}
50
51impl TryFrom<bitwarden_api_api::models::PolicyResponseModel> for Policy {
52    type Error = PolicyParseError;
53
54    fn try_from(
55        policy: bitwarden_api_api::models::PolicyResponseModel,
56    ) -> Result<Self, Self::Error> {
57        Ok(Policy {
58            id: PolicyId::new(require!(policy.id)),
59            organization_id: OrganizationId::new(require!(policy.organization_id)),
60            r#type: require!(policy.r#type).try_into()?,
61            data: policy.data.map(|d| d.to_string()),
62            enabled: require!(policy.enabled),
63            revision_date: policy.revision_date.map(|d| d.parse()).transpose()?,
64        })
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use bitwarden_api_api::models::{PolicyResponseModel, PolicyType as ApiPolicyType};
71
72    use super::*;
73
74    const TEST_POLICY_ID: &str = "0ae2ad8e-2f1c-4d1b-9d7c-7a4c3f4b8a11";
75    const TEST_ORGANIZATION_ID: &str = "5b9e0a0c-7f3e-4b8a-9c1d-2e6f4a8b0c22";
76
77    fn response() -> PolicyResponseModel {
78        PolicyResponseModel {
79            object: None,
80            id: Some(TEST_POLICY_ID.parse().unwrap()),
81            organization_id: Some(TEST_ORGANIZATION_ID.parse().unwrap()),
82            r#type: Some(ApiPolicyType::MasterPassword),
83            data: Some(serde_json::json!({ "minLength": 12 })),
84            enabled: Some(true),
85            revision_date: Some("2024-01-01T00:00:00Z".to_owned()),
86        }
87    }
88
89    #[test]
90    fn converts_policy_response() {
91        let policy = Policy::try_from(response()).unwrap();
92
93        assert_eq!(policy.id.to_string(), TEST_POLICY_ID);
94        assert_eq!(policy.organization_id.to_string(), TEST_ORGANIZATION_ID);
95        assert_eq!(policy.r#type, PolicyType::MasterPassword);
96        assert_eq!(policy.data.as_deref(), Some(r#"{"minLength":12}"#));
97        assert!(policy.enabled);
98        assert_eq!(
99            policy.revision_date.unwrap().to_rfc3339(),
100            "2024-01-01T00:00:00+00:00"
101        );
102    }
103
104    #[test]
105    fn rejects_unknown_policy_type() {
106        let response = PolicyResponseModel {
107            r#type: Some(ApiPolicyType::__Unknown(999)),
108            ..response()
109        };
110
111        assert!(Policy::try_from(response).is_err());
112    }
113
114    #[test]
115    fn rejects_malformed_revision_date() {
116        let response = PolicyResponseModel {
117            revision_date: Some("yesterday".to_owned()),
118            ..response()
119        };
120
121        assert!(Policy::try_from(response).is_err());
122    }
123}