Skip to main content

bitwarden_policies/
models.rs

1//! Data models for the policy domain.
2
3use std::{any::TypeId, collections::HashMap};
4
5use bitwarden_core::OrganizationId;
6use bitwarden_organizations::{OrganizationUserStatusType, OrganizationUserType};
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9#[cfg(feature = "wasm")]
10use tsify::Tsify;
11use uuid::Uuid;
12
13use crate::{
14    Policy,
15    policy_type::{PolicyDataType, PolicyType},
16};
17
18/// An organization policy in the raw data format that is sent over the FFI.
19///
20/// TODO: this is misnamed, but changing it is a breaking change.
21#[derive(Serialize, Deserialize, Debug, Clone)]
22#[serde(rename_all = "camelCase")]
23#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
24#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
25pub struct PolicyView {
26    /// The policy's unique ID.
27    pub id: Uuid,
28    /// The organization this policy belongs to.
29    pub organization_id: OrganizationId,
30    /// The type of policy.
31    pub r#type: PolicyType,
32    /// The policy's additional configuration data as a JSON string, if any.
33    pub data: Option<String>,
34    /// Whether the policy is enabled.
35    pub enabled: bool,
36    /// When the policy was last modified.
37    pub revision_date: Option<DateTime<Utc>>,
38}
39
40/// A minimal set of data for a user in an organization. This provides
41/// the context needed to evaluate the policies that are applied to the
42/// user.
43#[derive(Serialize, Deserialize, Debug, Clone)]
44#[serde(rename_all = "camelCase")]
45#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
46#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
47pub struct OrganizationUserPolicyContext {
48    /// The organization's unique ID.
49    pub id: OrganizationId,
50    /// The user's membership status in the organization.
51    pub status: OrganizationUserStatusType,
52    /// The user's role within the organization.
53    pub role: OrganizationUserType,
54    /// Whether the organization is enabled.
55    pub enabled: bool,
56    /// Whether the organization's plan supports policies.
57    pub use_policies: bool,
58    /// Whether the user is acting on behalf of a provider
59    /// that manages the organization.
60    pub is_provider_user: bool,
61}
62
63/// A per-organization enforcement decision for a single policy type.
64///
65/// Unlike [`PolicyView`] (the server-side record), this carries only the
66/// fields relevant to an enforcement decision: `enforced` reflects the
67/// user-specific evaluation rather than the policy's raw `enabled` flag, and
68/// `data` is strongly typed via [`Policy::Data`].
69///
70/// `data` is always populated. It is [`Default::default()`] whenever the policy
71/// is not enforced against the user, when no matching policy is found, or when
72/// the policy record's data could not be parsed.
73#[derive(Debug, Clone, PartialEq)]
74pub(crate) struct EnforcedPolicy<P: Policy> {
75    /// The organization this enforcement decision is for.
76    pub organization_id: OrganizationId,
77    /// The policy data, if any.
78    pub data: P::Data,
79    /// Whether the policy should be enforced against the current user for this
80    /// organization.
81    pub enforced: bool,
82}
83
84impl<P: Policy> EnforcedPolicy<P> {
85    /// The decision for an organization that has no matching policy: not
86    /// enforced, with [`Default`] data.
87    pub(crate) fn not_enforced(organization_id: OrganizationId) -> Self {
88        Self {
89            organization_id,
90            data: Default::default(),
91            enforced: false,
92        }
93    }
94
95    /// Consumes this decision into its FFI-friendly form, erasing the
96    /// strongly-typed `data` into a [`PolicyDataType`] via [`Policy::to_erased`].
97    pub(crate) fn into_erased(self, policy: &P) -> EnforcedPolicyErased {
98        EnforcedPolicyErased {
99            organization_id: self.organization_id,
100            data: policy.to_erased(self.data),
101            enforced: self.enforced,
102        }
103    }
104}
105
106/// The FFI-facing counterpart of the native `EnforcedPolicy`, with its
107/// strongly-typed `data` erased to [`PolicyDataType`] so it can cross the
108/// binding boundary.
109#[derive(Serialize, Deserialize, Debug, Clone)]
110#[serde(rename_all = "camelCase")]
111#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
112#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
113pub struct EnforcedPolicyErased {
114    /// The organization this enforcement decision is for.
115    pub organization_id: OrganizationId,
116    /// The policy data, if any.
117    pub data: PolicyDataType,
118    /// Whether the policy is being enforced against the current user for this
119    /// organization.
120    pub enforced: bool,
121}
122
123/// A [`PolicyView`] resolved against the concrete [`Policy`] that handles it.
124///
125/// This is the typed domain value the untyped wire [`PolicyView`] transforms into
126/// at the boundary: the `r#type` discriminant has been matched and the untyped
127/// `data` blob parsed into [`Policy::Data`], so a policy can only ever be paired
128/// with its own data type.
129pub(crate) struct ResolvedPolicyView<P: Policy> {
130    organization_id: OrganizationId,
131    enabled: bool,
132    data: P::Data,
133}
134
135impl<P: Policy> ResolvedPolicyView<P> {
136    /// Resolves `view` against `policy`, returning `Some` only when the view is
137    /// the type handled by `policy`.
138    ///
139    /// Deserializes the untyped `data` blob into `P::Data`, falling back to
140    /// [`Default`] (with a warning) when it is absent or fails to parse.
141    ///
142    /// By default, a single unrecognised value within `P::Data` will fail
143    /// parsing of the entire struct. Individual policies should provide their
144    /// own handling at the field level if this is unacceptable.
145    pub(crate) fn resolve(policy: &P, view: &PolicyView) -> Option<Self> {
146        if view.r#type != policy.policy_type() {
147            return None;
148        }
149
150        let data = match view.data.as_deref() {
151            Some(raw) => serde_json::from_str(raw).unwrap_or_else(|e| {
152                if TypeId::of::<P::Data>() == TypeId::of::<()>() {
153                    // Any non-null value will fail to deserialize to ().
154                    // This is a separate case to receiving malformed data - log it separately.
155                    tracing::debug!(
156                        policy_type = ?policy.policy_type(),
157                        organization_id = %view.organization_id,
158                        "Ignoring unexpected data for a policy type that models none"
159                    );
160                } else {
161                    tracing::warn!(
162                        policy_type = ?policy.policy_type(),
163                        organization_id = %view.organization_id,
164                        "Failed to parse policy data, falling back to default: {e}"
165                    );
166                }
167                Default::default()
168            }),
169            None => Default::default(),
170        };
171
172        Some(Self {
173            organization_id: view.organization_id,
174            enabled: view.enabled,
175            data,
176        })
177    }
178
179    /// Consumes the resolved view into an [`EnforcedPolicy`], evaluating whether
180    /// `policy` is enforced against the user.
181    ///
182    /// Pass in all organization contexts for this user; the specific context is looked
183    /// up by this method so that a mismatch between policy and organization is not possible.
184    /// If no context is present for the organization, the policy is enforced by default
185    /// (err on the side of enforcement).
186    pub(crate) fn into_enforced(
187        self,
188        policy: &P,
189        organization_user_policy_contexts: &HashMap<OrganizationId, &OrganizationUserPolicyContext>,
190    ) -> EnforcedPolicy<P> {
191        let context = organization_user_policy_contexts.get(&self.organization_id);
192
193        let enforced = self.enabled
194            && context.is_none_or(|ctx| {
195                ctx.enabled
196                    && ctx.use_policies
197                    && policy.enforced_statuses().contains(&ctx.status)
198                    && !policy.exempt_roles().contains(&ctx.role)
199                    && !(ctx.is_provider_user && policy.exempt_providers())
200            });
201
202        if enforced {
203            EnforcedPolicy {
204                organization_id: self.organization_id,
205                data: self.data,
206                enforced,
207            }
208        } else {
209            EnforcedPolicy::not_enforced(self.organization_id)
210        }
211    }
212}