bitwarden_pam/rotation/
actions.rs1use serde::{Deserialize, Serialize};
4
5use super::{
6 configs::RotationConfigView,
7 models::{TargetSystemMethod, TargetSystemStatus},
8};
9
10#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
12#[bitwarden_ffi::wasm_record]
13#[serde(rename_all = "camelCase")]
14pub struct RotationConfigActions {
15 pub can_rotate_now: bool,
17 pub can_record_manual: bool,
19 pub mutations_locked: bool,
21 pub can_pause: bool,
23 pub can_resume: bool,
25}
26
27pub fn rotation_config_actions(
30 config: &RotationConfigView,
31 target_status: Option<TargetSystemStatus>,
32) -> RotationConfigActions {
33 RotationConfigActions {
34 can_rotate_now: config.enabled
37 && config.target_system_method == TargetSystemMethod::Automatic
38 && target_status == Some(TargetSystemStatus::Active)
39 && !config.has_active_job,
40 can_record_manual: config.target_system_method == TargetSystemMethod::Manual,
41 mutations_locked: config.has_active_job,
44 can_pause: config.enabled,
45 can_resume: !config.enabled,
46 }
47}
48
49#[cfg(test)]
50mod tests {
51 use chrono::{DateTime, Utc};
52 use uuid::uuid;
53
54 use super::*;
55 use crate::{RotationConfigId, RotationConfigSealedFields, TargetSystemId};
56
57 fn timestamp() -> DateTime<Utc> {
58 "2026-01-01T00:00:00Z".parse().expect("a valid timestamp")
59 }
60
61 fn rotatable_config() -> RotationConfigView {
64 RotationConfigView {
65 id: RotationConfigId::new(uuid!("11111111-1111-1111-1111-111111111111")),
66 organization_id: bitwarden_core::OrganizationId::new(uuid!(
67 "22222222-2222-2222-2222-222222222222"
68 )),
69 cipher_id: bitwarden_vault::CipherId::new(uuid!(
70 "33333333-3333-3333-3333-333333333333"
71 )),
72 target_system_id: TargetSystemId::new(uuid!("44444444-4444-4444-4444-444444444444")),
73 target_system_name: "Prod SQL".to_string(),
74 target_system_method: TargetSystemMethod::Automatic,
75 sealed: Some(RotationConfigSealedFields {
76 account_identity: "svc_rotation".to_string(),
77 terminate_sessions: false,
78 }),
79 schedule_cron: None,
80 rotate_on_access_end: false,
81 enabled: true,
82 last_rotation_at: None,
83 next_rotation_at: None,
84 has_active_job: false,
85 awaiting_manual_rotation: false,
86 creation_date: timestamp(),
87 revision_date: timestamp(),
88 }
89 }
90
91 #[test]
92 fn an_enabled_idle_automatic_config_on_an_active_target_can_rotate() {
93 let actions =
94 rotation_config_actions(&rotatable_config(), Some(TargetSystemStatus::Active));
95
96 assert!(actions.can_rotate_now);
97 assert!(!actions.can_record_manual);
98 assert!(!actions.mutations_locked);
99 assert!(actions.can_pause);
100 assert!(!actions.can_resume);
101 }
102
103 #[test]
104 fn a_paused_config_cannot_rotate_and_offers_resume_instead() {
105 let config = RotationConfigView {
106 enabled: false,
107 ..rotatable_config()
108 };
109
110 let actions = rotation_config_actions(&config, Some(TargetSystemStatus::Active));
111
112 assert!(!actions.can_rotate_now);
113 assert!(!actions.can_pause);
114 assert!(actions.can_resume);
115 }
116
117 #[test]
118 fn a_config_with_a_job_in_flight_cannot_rotate_and_locks_mutations() {
119 let config = RotationConfigView {
120 has_active_job: true,
121 ..rotatable_config()
122 };
123
124 let actions = rotation_config_actions(&config, Some(TargetSystemStatus::Active));
125
126 assert!(!actions.can_rotate_now);
127 assert!(actions.mutations_locked);
128 }
129
130 #[test]
131 fn a_disabled_target_system_blocks_rotation() {
132 let actions =
133 rotation_config_actions(&rotatable_config(), Some(TargetSystemStatus::Disabled));
134
135 assert!(!actions.can_rotate_now);
136 }
137
138 #[test]
141 fn an_unloaded_target_system_blocks_rotation() {
142 let actions = rotation_config_actions(&rotatable_config(), None);
143
144 assert!(!actions.can_rotate_now);
145 }
146
147 #[test]
148 fn a_manual_config_offers_record_manual_rather_than_rotate() {
149 let config = RotationConfigView {
150 target_system_method: TargetSystemMethod::Manual,
151 ..rotatable_config()
152 };
153
154 let actions = rotation_config_actions(&config, Some(TargetSystemStatus::Active));
155
156 assert!(!actions.can_rotate_now);
157 assert!(actions.can_record_manual);
158 }
159
160 #[test]
163 fn an_unrecognized_method_offers_neither_rotation_action() {
164 let config = RotationConfigView {
165 target_system_method: TargetSystemMethod::Unknown,
166 ..rotatable_config()
167 };
168
169 let actions = rotation_config_actions(&config, Some(TargetSystemStatus::Active));
170
171 assert!(!actions.can_rotate_now);
172 assert!(!actions.can_record_manual);
173 }
174
175 #[test]
176 fn an_unrecognized_target_status_blocks_rotation() {
177 let actions =
178 rotation_config_actions(&rotatable_config(), Some(TargetSystemStatus::Unknown));
179
180 assert!(!actions.can_rotate_now);
181 }
182
183 #[test]
185 fn pause_and_resume_are_mutually_exclusive() {
186 for enabled in [true, false] {
187 let config = RotationConfigView {
188 enabled,
189 ..rotatable_config()
190 };
191
192 let actions = rotation_config_actions(&config, Some(TargetSystemStatus::Active));
193
194 assert_ne!(actions.can_pause, actions.can_resume, "enabled: {enabled}");
195 }
196 }
197}