1use bytes::{Buf, BufMut};
21
22#[cfg(test)]
23use crate::krabka::test_support::sample_tagged_fields;
24use crate::{
25 Decode, Encode, ProtocolError, ProtocolRequest, UnknownTaggedFields,
26 krabka::common::{
27 BOOL_LEN, I16_LEN, I32_LEN, UUID_LEN, check_version, read_unknown_tagged_fields,
28 unknown_tagged_fields_len, write_unknown_tagged_fields,
29 },
30 primitives::{
31 fixed::{get_bool, get_i16, get_i32, put_bool, put_i16, put_i32},
32 string_bytes::{
33 compact_bytes_len, compact_nullable_string_len, compact_string_len,
34 get_compact_bytes_owned, get_compact_nullable_string_owned, get_compact_string_owned,
35 put_compact_bytes, put_compact_nullable_string, put_compact_string,
36 },
37 uuid::{Uuid, get_uuid, put_uuid},
38 },
39};
40
41pub const API_KEY: i16 = 1018;
43pub const MIN_VERSION: i16 = 0;
45pub const MAX_VERSION: i16 = 0;
47pub const FLEXIBLE_MIN: i16 = 0;
49
50#[inline]
55#[must_use]
56pub fn is_flexible(version: i16) -> bool {
57 version >= FLEXIBLE_MIN
58}
59
60#[derive(Debug, Clone, PartialEq, Eq, Default)]
62pub struct ApproveBreakGlassRequest {
63 pub proposal_id: Uuid,
65 pub key_id: String,
68 pub signature: Vec<u8>,
71 pub withdraw: bool,
74 pub unknown_tagged_fields: UnknownTaggedFields,
76}
77
78impl Encode for ApproveBreakGlassRequest {
79 fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
80 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
81 put_uuid(buf, self.proposal_id);
82 put_compact_string(buf, &self.key_id)?;
83 put_compact_bytes(buf, &self.signature)?;
84 put_bool(buf, self.withdraw);
85 write_unknown_tagged_fields(buf, &self.unknown_tagged_fields)?;
86 Ok(())
87 }
88
89 fn encoded_len(&self, _version: i16) -> usize {
90 UUID_LEN
91 + compact_string_len(&self.key_id)
92 + compact_bytes_len(&self.signature)
93 + BOOL_LEN
94 + unknown_tagged_fields_len(&self.unknown_tagged_fields)
95 }
96}
97
98impl Decode<'_> for ApproveBreakGlassRequest {
99 fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
100 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
101 Ok(Self {
102 proposal_id: get_uuid(buf)?,
103 key_id: get_compact_string_owned(buf)?,
104 signature: get_compact_bytes_owned(buf)?.to_vec(),
105 withdraw: get_bool(buf)?,
106 unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
107 })
108 }
109}
110
111impl ProtocolRequest for ApproveBreakGlassRequest {
112 const API_KEY: i16 = API_KEY;
113 const MIN_VERSION: i16 = MIN_VERSION;
114 const MAX_VERSION: i16 = MAX_VERSION;
115 const LATEST_STABLE_VERSION: i16 = MAX_VERSION;
116 const FLEXIBLE_MIN: i16 = FLEXIBLE_MIN;
117
118 type Response = ApproveBreakGlassResponse;
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, Default)]
123pub struct ApproveBreakGlassResponse {
124 pub throttle_time_ms: i32,
126 pub error_code: i16,
129 pub error_message: Option<String>,
132 pub approvals_held: i32,
136 pub approvals_required: i32,
139 pub unknown_tagged_fields: UnknownTaggedFields,
141}
142
143impl Encode for ApproveBreakGlassResponse {
144 fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
145 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
146 put_i32(buf, self.throttle_time_ms);
147 put_i16(buf, self.error_code);
148 put_compact_nullable_string(buf, self.error_message.as_deref())?;
149 put_i32(buf, self.approvals_held);
150 put_i32(buf, self.approvals_required);
151 write_unknown_tagged_fields(buf, &self.unknown_tagged_fields)?;
152 Ok(())
153 }
154
155 fn encoded_len(&self, _version: i16) -> usize {
156 I32_LEN
157 + I16_LEN
158 + compact_nullable_string_len(self.error_message.as_deref())
159 + I32_LEN
160 + I32_LEN
161 + unknown_tagged_fields_len(&self.unknown_tagged_fields)
162 }
163}
164
165impl Decode<'_> for ApproveBreakGlassResponse {
166 fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
167 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
168 Ok(Self {
169 throttle_time_ms: get_i32(buf)?,
170 error_code: get_i16(buf)?,
171 error_message: get_compact_nullable_string_owned(buf)?,
172 approvals_held: get_i32(buf)?,
173 approvals_required: get_i32(buf)?,
174 unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
175 })
176 }
177}
178
179#[cfg(test)]
180impl ApproveBreakGlassRequest {
181 fn populated() -> Self {
183 Self {
184 proposal_id: Uuid([
185 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215,
186 ]),
187 key_id: "bob-yubi".to_owned(),
188 signature: (0..64).collect(),
189 withdraw: false,
190 unknown_tagged_fields: sample_tagged_fields(),
191 }
192 }
193}
194
195#[cfg(test)]
196impl ApproveBreakGlassResponse {
197 fn populated() -> Self {
199 Self {
200 throttle_time_ms: 9,
201 error_code: 1007,
202 error_message: Some("principal already approved this proposal".to_owned()),
203 approvals_held: 1,
204 approvals_required: 2,
205 unknown_tagged_fields: sample_tagged_fields(),
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use assert2::assert;
213
214 use super::*;
215 use crate::krabka::test_support::{reject_truncated, roundtrip_case, unsupported_versions};
216
217 #[test]
218 fn requests_roundtrip() {
219 let cases = [
220 ("default", ApproveBreakGlassRequest::default()),
221 ("signed approval", ApproveBreakGlassRequest::populated()),
222 (
223 "unsigned approval",
224 ApproveBreakGlassRequest {
225 key_id: String::new(),
226 signature: Vec::new(),
227 unknown_tagged_fields: UnknownTaggedFields::default(),
228 ..ApproveBreakGlassRequest::populated()
229 },
230 ),
231 (
232 "withdraw",
233 ApproveBreakGlassRequest {
234 key_id: String::new(),
235 signature: Vec::new(),
236 withdraw: true,
237 ..ApproveBreakGlassRequest::populated()
238 },
239 ),
240 ];
241 for (label, request) in &cases {
242 for version in MIN_VERSION..=MAX_VERSION {
243 roundtrip_case(label, request, version);
244 }
245 }
246 }
247
248 #[test]
249 fn responses_roundtrip() {
250 let cases = [
251 ("default", ApproveBreakGlassResponse::default()),
252 (
253 "refusal with a message",
254 ApproveBreakGlassResponse::populated(),
255 ),
256 (
257 "second approval reaches the quorum",
258 ApproveBreakGlassResponse {
259 throttle_time_ms: 0,
260 error_code: 0,
261 error_message: None,
262 approvals_held: 2,
263 approvals_required: 2,
264 unknown_tagged_fields: UnknownTaggedFields::default(),
265 },
266 ),
267 ];
268 for (label, response) in &cases {
269 for version in MIN_VERSION..=MAX_VERSION {
270 roundtrip_case(label, response, version);
271 }
272 }
273 }
274
275 #[test]
276 fn a_default_request_approves_and_does_not_withdraw() {
277 assert!(!ApproveBreakGlassRequest::default().withdraw);
278 }
279
280 #[test]
281 fn truncated_input_is_rejected() {
282 reject_truncated(&ApproveBreakGlassRequest::populated(), MIN_VERSION);
283 reject_truncated(&ApproveBreakGlassResponse::populated(), MIN_VERSION);
284 }
285
286 #[test]
287 fn rejects_unsupported_versions() {
288 for version in unsupported_versions(MIN_VERSION, MAX_VERSION) {
289 let request = ApproveBreakGlassRequest::populated();
290 assert!(matches!(
291 request.encode(&mut Vec::<u8>::new(), version),
292 Err(ProtocolError::UnsupportedVersion {
293 api_key: API_KEY,
294 ..
295 })
296 ));
297 let response = ApproveBreakGlassResponse::populated();
298 assert!(matches!(
299 response.encode(&mut Vec::<u8>::new(), version),
300 Err(ProtocolError::UnsupportedVersion {
301 api_key: API_KEY,
302 ..
303 })
304 ));
305 }
306 }
307}