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