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