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    pub(crate) fn resolve(policy: &P, view: &PolicyView) -> Option<Self> {
142        if view.r#type != policy.policy_type() {
143            return None;
144        }
145
146        let data = match view.data.as_deref() {
147            Some(raw) => serde_json::from_str(raw).unwrap_or_else(|e| {
148                if TypeId::of::<P::Data>() == TypeId::of::<()>() {
149                    // Any non-null value will fail to deserialize to ().
150                    // This is a separate case to receiving malformed data - log it separately.
151                    tracing::debug!(
152                        policy_type = ?policy.policy_type(),
153                        organization_id = %view.organization_id,
154                        "Ignoring unexpected data for a policy type that models none"
155                    );
156                } else {
157                    tracing::warn!(
158                        policy_type = ?policy.policy_type(),
159                        organization_id = %view.organization_id,
160                        "Failed to parse policy data, falling back to default: {e}"
161                    );
162                }
163                Default::default()
164            }),
165            None => Default::default(),
166        };
167
168        Some(Self {
169            organization_id: view.organization_id,
170            enabled: view.enabled,
171            data,
172        })
173    }
174
175    /// Consumes the resolved view into an [`EnforcedPolicy`], evaluating whether
176    /// `policy` is enforced against the user.
177    ///
178    /// Pass in all organization contexts for this user; the specific context is looked
179    /// up by this method so that a mismatch between policy and organization is not possible.
180    /// If no context is present for the organization, the policy is enforced by default
181    /// (err on the side of enforcement).
182    pub(crate) fn into_enforced(
183        self,
184        policy: &P,
185        organization_user_policy_contexts: &HashMap<OrganizationId, &OrganizationUserPolicyContext>,
186    ) -> EnforcedPolicy<P> {
187        let context = organization_user_policy_contexts.get(&self.organization_id);
188
189        let enforced = self.enabled
190            && context.is_none_or(|ctx| {
191                ctx.enabled
192                    && ctx.use_policies
193                    && policy.enforced_statuses().contains(&ctx.status)
194                    && !policy.exempt_roles().contains(&ctx.role)
195                    && !(ctx.is_provider_user && policy.exempt_providers())
196            });
197
198        if enforced {
199            EnforcedPolicy {
200                organization_id: self.organization_id,
201                data: self.data,
202                enforced,
203            }
204        } else {
205            EnforcedPolicy::not_enforced(self.organization_id)
206        }
207    }
208}