Skip to main content

bitwarden_pam/approvals/
models.rs

1use bitwarden_api_api::models::{
2    AccessDecisionRequestModel, AccessDecisionVerdict as ApiAccessDecisionVerdict,
3    AccessRequestRevokeRequestModel,
4};
5use serde::{Deserialize, Serialize};
6
7use super::error::ApprovalError;
8use crate::AccessDecisionVerdict;
9
10/// An approver's decision on a pending access request.
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
12#[bitwarden_ffi::wasm_record]
13#[serde(rename_all = "camelCase")]
14pub struct AccessDecisionRequest {
15    /// The verdict to record; [`AccessDecisionVerdict::Unknown`] cannot be submitted.
16    pub verdict: AccessDecisionVerdict,
17    /// An optional note shown to the requester.
18    pub comment: Option<String>,
19}
20
21impl TryFrom<AccessDecisionRequest> for AccessDecisionRequestModel {
22    type Error = ApprovalError;
23
24    fn try_from(request: AccessDecisionRequest) -> Result<Self, Self::Error> {
25        let verdict = match request.verdict {
26            AccessDecisionVerdict::Approve => ApiAccessDecisionVerdict::Approve,
27            AccessDecisionVerdict::Deny => ApiAccessDecisionVerdict::Deny,
28            AccessDecisionVerdict::Unknown => return Err(ApprovalError::UnsubmittableVerdict),
29        };
30
31        Ok(Self {
32            verdict,
33            comment: request.comment,
34        })
35    }
36}
37
38/// An approver revoking an approved access request the requester has not started.
39#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
40#[bitwarden_ffi::wasm_record]
41#[serde(rename_all = "camelCase")]
42pub struct AccessRequestRevokeRequest {
43    /// Why the approval is revoked. Required by the server; recorded with the approver's decision
44    /// and shown to the requester.
45    pub reason: String,
46}
47
48impl From<AccessRequestRevokeRequest> for AccessRequestRevokeRequestModel {
49    fn from(request: AccessRequestRevokeRequest) -> Self {
50        Self {
51            reason: Some(request.reason),
52        }
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn approve_converts_to_model() {
62        let request = AccessDecisionRequest {
63            verdict: AccessDecisionVerdict::Approve,
64            comment: Some("Looks fine".to_string()),
65        };
66
67        let model = AccessDecisionRequestModel::try_from(request).unwrap();
68
69        assert_eq!(model.verdict, ApiAccessDecisionVerdict::Approve);
70        assert_eq!(model.comment, Some("Looks fine".to_string()));
71    }
72
73    #[test]
74    fn deny_converts_to_model() {
75        let request = AccessDecisionRequest {
76            verdict: AccessDecisionVerdict::Deny,
77            comment: None,
78        };
79
80        let model = AccessDecisionRequestModel::try_from(request).unwrap();
81
82        assert_eq!(model.verdict, ApiAccessDecisionVerdict::Deny);
83        assert_eq!(model.comment, None);
84    }
85
86    #[test]
87    fn unknown_verdict_is_rejected() {
88        let request = AccessDecisionRequest {
89            verdict: AccessDecisionVerdict::Unknown,
90            comment: None,
91        };
92
93        let result = AccessDecisionRequestModel::try_from(request);
94
95        assert!(matches!(result, Err(ApprovalError::UnsubmittableVerdict)));
96    }
97
98    #[test]
99    fn revoke_converts_to_model() {
100        let model = AccessRequestRevokeRequestModel::from(AccessRequestRevokeRequest {
101            reason: "Approved by mistake".to_string(),
102        });
103
104        assert_eq!(model.reason.as_deref(), Some("Approved by mistake"));
105    }
106}