bitwarden_access_connector/integrations/scripting/
outcome.rs1use bitwarden_pam::RotationFailureCode;
4
5use super::{
6 Operation,
7 runner::{InvokeError, ScriptExit},
8};
9use crate::{
10 error::{Retry, SafeDetail},
11 integrations::{IntegrationError, TargetEffect},
12};
13
14pub(crate) fn classify_outcome(
17 outcome: Result<ScriptExit, InvokeError>,
18 operation: Operation,
19 timeout_secs: u64,
20) -> Result<(), IntegrationError> {
21 match outcome {
22 Ok(ScriptExit::Code(0)) => Ok(()),
23 Ok(exit) => Err(exit_error(exit, operation)),
24 Err(InvokeError::Timeout) => Err(timeout_error(operation, timeout_secs)),
25 Err(e @ InvokeError::StdinWrite(_)) => {
26 Err(script_io_error(operation, "ScriptStdinWriteFailed").with_source(e))
27 }
28 Err(e @ InvokeError::Wait(_)) => {
29 Err(script_io_error(operation, "ScriptWaitFailed").with_source(e))
30 }
31 Err(e @ (InvokeError::Serialize(_) | InvokeError::Spawn(_))) => Err(IntegrationError::new(
32 Retry::Permanent,
33 TargetEffect::NotApplied,
34 RotationFailureCode::Internal,
35 SafeDetail::from_kind("ScriptSpawnError"),
36 )
37 .with_source(e)),
38 }
39}
40
41fn script_io_error(operation: Operation, kind: &'static str) -> IntegrationError {
42 IntegrationError::new(
43 Retry::Permanent,
44 operation.effect_if_interrupted(),
45 RotationFailureCode::Internal,
46 SafeDetail::from_kind(kind),
47 )
48}
49
50fn timeout_error(operation: Operation, timeout_secs: u64) -> IntegrationError {
51 IntegrationError::new(
52 Retry::Permanent,
53 operation.effect_on_timeout(),
54 RotationFailureCode::ScriptTimeout,
55 SafeDetail::timed_out(timeout_secs),
56 )
57}
58
59fn exit_error(exit: ScriptExit, operation: Operation) -> IntegrationError {
60 let (retry, effect, detail) = match exit {
61 ScriptExit::Code(1) => (
62 Retry::Permanent,
63 TargetEffect::NotApplied,
64 SafeDetail::from_exit_code(1),
65 ),
66 ScriptExit::Code(2) => (
67 Retry::Permanent,
68 TargetEffect::Applied,
69 SafeDetail::from_exit_code(2),
70 ),
71 ScriptExit::Code(3) => (
72 Retry::Permanent,
73 TargetEffect::Unknown,
74 SafeDetail::from_exit_code(3),
75 ),
76 ScriptExit::Code(4) => (
77 Retry::Transient,
78 TargetEffect::NotApplied,
79 SafeDetail::from_exit_code(4),
80 ),
81 other => {
83 let detail = match other {
84 ScriptExit::Code(n) => SafeDetail::from_exit_code(n),
85 ScriptExit::Signal(Some(n)) => SafeDetail::from_signal(n),
86 ScriptExit::Signal(None) => SafeDetail::from_kind("signal"),
87 };
88 (Retry::Permanent, operation.effect_if_interrupted(), detail)
89 }
90 };
91 IntegrationError::new(retry, effect, RotationFailureCode::ScriptFailed, detail)
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 fn io_error() -> std::io::Error {
99 std::io::Error::other("test io failure")
100 }
101
102 #[test]
103 fn classify_outcome_maps_the_exit_code_table() {
104 let cases = [
105 (1, Retry::Permanent, TargetEffect::NotApplied),
106 (2, Retry::Permanent, TargetEffect::Applied),
107 (3, Retry::Permanent, TargetEffect::Unknown),
108 (4, Retry::Transient, TargetEffect::NotApplied),
109 ];
110 for (code, retry, effect) in cases {
111 let err =
112 classify_outcome(Ok(ScriptExit::Code(code)), Operation::Rotate, 60).unwrap_err();
113 assert_eq!(err.retry, retry, "exit {code}");
114 assert_eq!(err.effect, effect, "exit {code}");
115 assert_eq!(err.code, RotationFailureCode::ScriptFailed, "exit {code}");
116 }
117 classify_outcome(Ok(ScriptExit::Code(0)), Operation::Rotate, 60).unwrap();
118 }
119
120 #[test]
121 fn classify_outcome_reports_the_signal_number() {
122 let err =
123 classify_outcome(Ok(ScriptExit::Signal(Some(9))), Operation::Rotate, 60).unwrap_err();
124 assert_eq!(err.retry, Retry::Permanent);
125 assert_eq!(err.effect, TargetEffect::Unknown);
126 assert_eq!(err.code, RotationFailureCode::ScriptFailed);
127 assert_eq!(err.detail.as_str(), "signal 9");
128 }
129
130 #[test]
131 fn classify_outcome_names_a_signal_without_a_number() {
132 let err =
133 classify_outcome(Ok(ScriptExit::Signal(None)), Operation::Rotate, 60).unwrap_err();
134 assert_eq!(err.detail.as_str(), "error kind: signal");
135 }
136
137 #[test]
138 fn classify_outcome_signal_leaves_a_non_rotate_target_alone() {
139 let err =
140 classify_outcome(Ok(ScriptExit::Signal(Some(9))), Operation::Verify, 60).unwrap_err();
141 assert_eq!(err.effect, TargetEffect::NotApplied);
142 }
143
144 #[test]
145 fn classify_outcome_unknown_exit_code_effect_depends_on_the_operation() {
146 for (operation, effect) in [
147 (Operation::Rotate, TargetEffect::Unknown),
148 (Operation::Verify, TargetEffect::NotApplied),
149 (Operation::Terminate, TargetEffect::NotApplied),
150 ] {
151 let err = classify_outcome(Ok(ScriptExit::Code(99)), operation, 60).unwrap_err();
152 assert_eq!(err.effect, effect, "{operation:?}");
153 }
154 }
155
156 #[test]
157 fn classify_outcome_timeout_effect_depends_on_the_operation() {
158 for (operation, effect) in [
159 (Operation::Rotate, TargetEffect::Unknown),
160 (Operation::Verify, TargetEffect::Applied),
161 (Operation::Terminate, TargetEffect::NotApplied),
162 ] {
163 let err = classify_outcome(Err(InvokeError::Timeout), operation, 60).unwrap_err();
164 assert_eq!(
165 err.code,
166 RotationFailureCode::ScriptTimeout,
167 "{operation:?}"
168 );
169 assert_eq!(err.effect, effect, "{operation:?}");
170 }
171 }
172
173 #[test]
174 fn classify_outcome_does_not_claim_rotate_left_the_target_alone() {
175 let errors: [fn() -> InvokeError; 2] = [
176 || InvokeError::StdinWrite(io_error()),
177 || InvokeError::Wait(io_error()),
178 ];
179 for make in errors {
180 let err = make();
181 let rotate = classify_outcome(Err(make()), Operation::Rotate, 60).unwrap_err();
182 assert_eq!(rotate.effect, TargetEffect::Unknown, "{err:?}");
183
184 for operation in [Operation::Verify, Operation::Terminate] {
185 let other = classify_outcome(Err(make()), operation, 60).unwrap_err();
186 assert_eq!(
187 other.effect,
188 TargetEffect::NotApplied,
189 "{err:?}/{operation:?}"
190 );
191 }
192 }
193 }
194
195 #[test]
196 fn classify_outcome_names_the_failing_stage() {
197 let write = classify_outcome(
198 Err(InvokeError::StdinWrite(io_error())),
199 Operation::Rotate,
200 60,
201 )
202 .unwrap_err();
203 assert_eq!(write.detail.as_str(), "error kind: ScriptStdinWriteFailed");
204
205 let wait = classify_outcome(Err(InvokeError::Wait(io_error())), Operation::Rotate, 60)
206 .unwrap_err();
207 assert_eq!(wait.detail.as_str(), "error kind: ScriptWaitFailed");
208 }
209
210 #[test]
211 fn classify_outcome_reports_a_spawn_failure_as_internal() {
212 let spawn = classify_outcome(Err(InvokeError::Spawn(io_error())), Operation::Rotate, 60)
213 .unwrap_err();
214 assert_eq!(spawn.code, RotationFailureCode::Internal);
215 assert_eq!(spawn.detail.as_str(), "error kind: ScriptSpawnError");
216 }
217
218 #[test]
219 fn classify_outcome_keeps_the_invoke_error_as_the_source() {
220 for err in [
221 InvokeError::Spawn(io_error()),
222 InvokeError::StdinWrite(io_error()),
223 InvokeError::Wait(io_error()),
224 ] {
225 let err = classify_outcome(Err(err), Operation::Rotate, 60).unwrap_err();
226 assert!(std::error::Error::source(&err).is_some());
227 }
228 }
229}