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