Skip to main content

bitwarden_pam/leases/
models.rs

1use std::num::NonZeroU32;
2
3use bitwarden_api_api::models::{
4    AccessLeaseExtensionRequestModel, AccessLeaseResponseModel, AccessLeaseRevokeRequestModel,
5    AccessLeaseStatus as ApiAccessLeaseStatus,
6};
7use bitwarden_collections::collection::CollectionId;
8use bitwarden_core::{OrganizationId, UserId, require};
9use bitwarden_vault::CipherId;
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12#[cfg(feature = "wasm")]
13use tsify::Tsify;
14
15use crate::{AccessLeaseId, AccessRequestId, error::LeasingError};
16
17/// The lifecycle state of an access lease.
18#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
19#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
20#[serde(rename_all = "snake_case")]
21pub enum AccessLeaseStatus {
22    /// The lease is currently within its access window and grants access.
23    Active,
24    /// The lease's access window has closed; it no longer grants access.
25    Expired,
26    /// The lease was revoked before its window closed.
27    Revoked,
28    /// The lease was cancelled by its requester before its window closed.
29    Canceled,
30    /// A status value this SDK version does not recognize. Kept as a distinct variant so listing
31    /// leases never fails on a newer server's status.
32    Unknown,
33}
34
35impl From<ApiAccessLeaseStatus> for AccessLeaseStatus {
36    fn from(status: ApiAccessLeaseStatus) -> Self {
37        match status {
38            ApiAccessLeaseStatus::Active => Self::Active,
39            ApiAccessLeaseStatus::Expired => Self::Expired,
40            ApiAccessLeaseStatus::Revoked => Self::Revoked,
41            ApiAccessLeaseStatus::Cancelled => Self::Canceled,
42            ApiAccessLeaseStatus::__Unknown(_) => Self::Unknown,
43        }
44    }
45}
46
47/// A decrypted view of an access lease, as its requester sees it.
48///
49/// A lease is the single-use grant that an approved [`AccessRequest`](crate::AccessRequestView)
50/// mints when the requester activates it. While a lease is [`Active`](AccessLeaseStatus::Active)
51/// the requester may open the otherwise-gated cipher; once it
52/// [`Expired`](AccessLeaseStatus::Expired) or is [`Revoked`](AccessLeaseStatus::Revoked) the cipher
53/// re-locks.
54#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
55#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
56#[serde(rename_all = "camelCase")]
57pub struct AccessLeaseView {
58    /// The lease's unique identifier.
59    pub id: AccessLeaseId,
60    /// The request this lease was minted from.
61    pub request_id: AccessRequestId,
62    /// The cipher the lease grants access to.
63    pub cipher_id: CipherId,
64    /// The collection the cipher belongs to.
65    pub collection_id: CollectionId,
66    /// The organization that owns the cipher. None when the server omits it.
67    pub organization_id: Option<OrganizationId>,
68    /// The user the lease was granted to (the original requester).
69    pub requester_id: UserId,
70    /// The lease's lifecycle state.
71    pub status: AccessLeaseStatus,
72    /// When the lease's access window opens (UTC).
73    pub not_before: DateTime<Utc>,
74    /// When the lease's access window closes (UTC).
75    pub not_after: DateTime<Utc>,
76    /// When the lease was revoked early (UTC); None unless it was revoked before expiry.
77    pub revoked_at: Option<DateTime<Utc>>,
78    /// The user who revoked the lease; None unless it was revoked early.
79    pub revoked_by_user_id: Option<UserId>,
80}
81
82impl TryFrom<AccessLeaseResponseModel> for AccessLeaseView {
83    type Error = LeasingError;
84
85    fn try_from(response: AccessLeaseResponseModel) -> Result<Self, Self::Error> {
86        Ok(Self {
87            id: AccessLeaseId::new(require!(response.id)),
88            request_id: AccessRequestId::new(require!(response.request_id)),
89            cipher_id: CipherId::new(require!(response.cipher_id)),
90            collection_id: CollectionId::new(require!(response.collection_id)),
91            organization_id: response.organization_id.map(OrganizationId::new),
92            requester_id: UserId::new(require!(response.requester_id)),
93            status: AccessLeaseStatus::from(require!(response.status)),
94            not_before: require!(response.not_before).parse()?,
95            not_after: require!(response.not_after).parse()?,
96            revoked_at: response.revoked_at.map(|d| d.parse()).transpose()?,
97            revoked_by_user_id: response.revoked_by_user_id.map(UserId::new),
98        })
99    }
100}
101
102/// Request to extend an active lease.
103#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
104#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
105#[serde(rename_all = "camelCase")]
106pub struct AccessLeaseExtensionRequest {
107    /// How much further to push out the lease's end, in seconds. None asks the server to apply the
108    /// governing rule's default extension. Must be positive and within the rule's maximum.
109    pub duration_seconds: Option<NonZeroU32>,
110    /// The justification recorded with the extension. Required by the server to be non-empty.
111    pub reason: String,
112}
113
114impl From<AccessLeaseExtensionRequest> for AccessLeaseExtensionRequestModel {
115    fn from(request: AccessLeaseExtensionRequest) -> Self {
116        Self {
117            duration_seconds: request.duration_seconds.map(|d| d.get() as i32),
118            reason: request.reason,
119        }
120    }
121}
122
123/// Request to revoke (end) a lease before it expires.
124#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default)]
125#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
126#[serde(rename_all = "camelCase")]
127pub struct AccessLeaseRevokeRequest {
128    /// An optional note explaining the revocation. Recorded on the audit trail only.
129    pub reason: Option<String>,
130}
131
132impl From<AccessLeaseRevokeRequest> for AccessLeaseRevokeRequestModel {
133    fn from(request: AccessLeaseRevokeRequest) -> Self {
134        Self {
135            reason: request.reason,
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use std::num::NonZeroU32;
143
144    use super::*;
145
146    #[test]
147    fn access_lease_extension_request_converts_to_model() {
148        let request = AccessLeaseExtensionRequest {
149            duration_seconds: NonZeroU32::new(3600),
150            reason: "Need more time".to_string(),
151        };
152
153        let model = AccessLeaseExtensionRequestModel::from(request);
154
155        assert_eq!(model.duration_seconds, Some(3600));
156        assert_eq!(model.reason, "Need more time".to_string());
157    }
158}