1use bytes::{Buf, BufMut};
13
14use crate::{
15 Decode, Encode, ProtocolError, ProtocolRequest, UnknownTaggedFields,
16 krabka::common::{
17 BOOL_LEN, I8_LEN, I16_LEN, I32_LEN, I64_LEN, UUID_LEN, array_len, check_version,
18 decode_array, encode_array, read_unknown_tagged_fields, unknown_tagged_fields_len,
19 write_unknown_tagged_fields,
20 },
21 primitives::{
22 fixed::{
23 get_bool, get_i8, get_i16, get_i32, get_i64, put_bool, put_i8, put_i16, put_i32,
24 put_i64,
25 },
26 string_bytes::{
27 compact_bytes_len, compact_nullable_string_len, compact_string_len,
28 get_compact_bytes_owned, get_compact_nullable_string_owned, get_compact_string_owned,
29 put_compact_bytes, put_compact_nullable_string, put_compact_string,
30 },
31 uuid::{Uuid, get_uuid, put_uuid},
32 },
33};
34
35pub const API_KEY: i16 = 1019;
37pub const MIN_VERSION: i16 = 0;
39pub const MAX_VERSION: i16 = 0;
41pub const FLEXIBLE_MIN: i16 = 0;
43
44#[inline]
49#[must_use]
50pub fn is_flexible(version: i16) -> bool {
51 version >= FLEXIBLE_MIN
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Default)]
56pub struct DescribeBreakGlassRequest {
57 pub pending_only: bool,
59 pub proposal_id: Uuid,
61 pub unknown_tagged_fields: UnknownTaggedFields,
63}
64
65impl Encode for DescribeBreakGlassRequest {
66 fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
67 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
68 put_bool(buf, self.pending_only);
69 put_uuid(buf, self.proposal_id);
70 write_unknown_tagged_fields(buf, &self.unknown_tagged_fields);
71 Ok(())
72 }
73
74 fn encoded_len(&self, _version: i16) -> usize {
75 BOOL_LEN + UUID_LEN + unknown_tagged_fields_len(&self.unknown_tagged_fields)
76 }
77}
78
79impl Decode<'_> for DescribeBreakGlassRequest {
80 fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
81 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
82 Ok(Self {
83 pending_only: get_bool(buf)?,
84 proposal_id: get_uuid(buf)?,
85 unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
86 })
87 }
88}
89
90impl ProtocolRequest for DescribeBreakGlassRequest {
91 const API_KEY: i16 = API_KEY;
92 const MIN_VERSION: i16 = MIN_VERSION;
93 const MAX_VERSION: i16 = MAX_VERSION;
94 const LATEST_STABLE_VERSION: i16 = MAX_VERSION;
95 const FLEXIBLE_MIN: i16 = FLEXIBLE_MIN;
96
97 type Response = DescribeBreakGlassResponse;
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Default)]
102pub struct BreakGlassApproval {
103 pub principal: String,
106 pub approved_at_ms: i64,
109 pub key_id: String,
112 pub signature: Vec<u8>,
115 pub unknown_tagged_fields: UnknownTaggedFields,
117}
118
119impl Encode for BreakGlassApproval {
120 fn encode<B: BufMut>(&self, buf: &mut B, _version: i16) -> Result<(), ProtocolError> {
121 put_compact_string(buf, &self.principal);
122 put_i64(buf, self.approved_at_ms);
123 put_compact_string(buf, &self.key_id);
124 put_compact_bytes(buf, &self.signature);
125 write_unknown_tagged_fields(buf, &self.unknown_tagged_fields);
126 Ok(())
127 }
128
129 fn encoded_len(&self, _version: i16) -> usize {
130 compact_string_len(&self.principal)
131 + I64_LEN
132 + compact_string_len(&self.key_id)
133 + compact_bytes_len(&self.signature)
134 + unknown_tagged_fields_len(&self.unknown_tagged_fields)
135 }
136}
137
138impl Decode<'_> for BreakGlassApproval {
139 fn decode<B: Buf>(buf: &mut B, _version: i16) -> Result<Self, ProtocolError> {
140 Ok(Self {
141 principal: get_compact_string_owned(buf)?,
142 approved_at_ms: get_i64(buf)?,
143 key_id: get_compact_string_owned(buf)?,
144 signature: get_compact_bytes_owned(buf)?.to_vec(),
145 unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
146 })
147 }
148}
149
150#[derive(Debug, Clone, PartialEq, Eq, Default)]
152pub struct DescribedBreakGlassProposal {
153 pub proposal_id: Uuid,
155 pub action: i8,
158 pub target: String,
160 pub proposer: String,
162 pub reason: String,
164 pub created_at_ms: i64,
167 pub expires_at_ms: i64,
169 pub consumed_at_ms: i64,
173 pub withdrawn: bool,
175 pub approvals: Vec<BreakGlassApproval>,
178 pub unknown_tagged_fields: UnknownTaggedFields,
180}
181
182impl Encode for DescribedBreakGlassProposal {
183 fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
184 put_uuid(buf, self.proposal_id);
185 put_i8(buf, self.action);
186 put_compact_string(buf, &self.target);
187 put_compact_string(buf, &self.proposer);
188 put_compact_string(buf, &self.reason);
189 put_i64(buf, self.created_at_ms);
190 put_i64(buf, self.expires_at_ms);
191 put_i64(buf, self.consumed_at_ms);
192 put_bool(buf, self.withdrawn);
193 encode_array(buf, &self.approvals, version)?;
194 write_unknown_tagged_fields(buf, &self.unknown_tagged_fields);
195 Ok(())
196 }
197
198 fn encoded_len(&self, version: i16) -> usize {
199 UUID_LEN
200 + I8_LEN
201 + compact_string_len(&self.target)
202 + compact_string_len(&self.proposer)
203 + compact_string_len(&self.reason)
204 + I64_LEN
205 + I64_LEN
206 + I64_LEN
207 + BOOL_LEN
208 + array_len(&self.approvals, version)
209 + unknown_tagged_fields_len(&self.unknown_tagged_fields)
210 }
211}
212
213impl Decode<'_> for DescribedBreakGlassProposal {
214 fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
215 Ok(Self {
216 proposal_id: get_uuid(buf)?,
217 action: get_i8(buf)?,
218 target: get_compact_string_owned(buf)?,
219 proposer: get_compact_string_owned(buf)?,
220 reason: get_compact_string_owned(buf)?,
221 created_at_ms: get_i64(buf)?,
222 expires_at_ms: get_i64(buf)?,
223 consumed_at_ms: get_i64(buf)?,
224 withdrawn: get_bool(buf)?,
225 approvals: decode_array(buf, version)?,
226 unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
227 })
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Default)]
233pub struct DescribeBreakGlassResponse {
234 pub throttle_time_ms: i32,
236 pub error_code: i16,
238 pub error_message: Option<String>,
241 pub proposals: Vec<DescribedBreakGlassProposal>,
243 pub unknown_tagged_fields: UnknownTaggedFields,
245}
246
247impl Encode for DescribeBreakGlassResponse {
248 fn encode<B: BufMut>(&self, buf: &mut B, version: i16) -> Result<(), ProtocolError> {
249 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
250 put_i32(buf, self.throttle_time_ms);
251 put_i16(buf, self.error_code);
252 put_compact_nullable_string(buf, self.error_message.as_deref());
253 encode_array(buf, &self.proposals, version)?;
254 write_unknown_tagged_fields(buf, &self.unknown_tagged_fields);
255 Ok(())
256 }
257
258 fn encoded_len(&self, version: i16) -> usize {
259 I32_LEN
260 + I16_LEN
261 + compact_nullable_string_len(self.error_message.as_deref())
262 + array_len(&self.proposals, version)
263 + unknown_tagged_fields_len(&self.unknown_tagged_fields)
264 }
265}
266
267impl Decode<'_> for DescribeBreakGlassResponse {
268 fn decode<B: Buf>(buf: &mut B, version: i16) -> Result<Self, ProtocolError> {
269 check_version(API_KEY, version, MIN_VERSION, MAX_VERSION)?;
270 Ok(Self {
271 throttle_time_ms: get_i32(buf)?,
272 error_code: get_i16(buf)?,
273 error_message: get_compact_nullable_string_owned(buf)?,
274 proposals: decode_array(buf, version)?,
275 unknown_tagged_fields: read_unknown_tagged_fields(buf)?,
276 })
277 }
278}
279
280#[cfg(test)]
281impl DescribeBreakGlassRequest {
282 fn populated() -> Self {
284 Self {
285 pending_only: true,
286 proposal_id: Uuid([
287 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215,
288 ]),
289 unknown_tagged_fields: crate::krabka::test_support::sample_tagged_fields(),
290 }
291 }
292}
293
294#[cfg(test)]
295impl DescribeBreakGlassResponse {
296 fn populated() -> Self {
298 Self {
299 throttle_time_ms: 6,
300 error_code: 0,
301 error_message: None,
302 proposals: vec![
303 DescribedBreakGlassProposal {
304 proposal_id: Uuid([
305 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214,
306 215,
307 ]),
308 action: 6,
309 target: "doomed".to_string(),
310 proposer: "User:alice".to_string(),
311 reason: "topic holds test data only".to_string(),
312 created_at_ms: 1_770_000_000_000,
313 expires_at_ms: 1_770_000_180_000,
314 consumed_at_ms: 0,
315 withdrawn: false,
316 approvals: vec![
317 BreakGlassApproval {
318 principal: "User:bob".to_string(),
319 approved_at_ms: 1_770_000_060_000,
320 key_id: "bob-yubi".to_string(),
321 signature: (0..64).collect(),
322 unknown_tagged_fields:
323 crate::krabka::test_support::sample_tagged_fields(),
324 },
325 BreakGlassApproval {
326 principal: "User:carol".to_string(),
327 approved_at_ms: 1_770_000_090_000,
328 ..BreakGlassApproval::default()
329 },
330 ],
331 unknown_tagged_fields: crate::krabka::test_support::sample_tagged_fields(),
332 },
333 DescribedBreakGlassProposal {
334 proposal_id: Uuid([1; 16]),
335 action: 1,
336 target: "literal:orders".to_string(),
337 proposer: "User:dave".to_string(),
338 reason: "DR cutover finished".to_string(),
339 created_at_ms: 1_769_000_000_000,
340 expires_at_ms: 1_769_000_180_000,
341 consumed_at_ms: 1_769_000_120_000,
342 withdrawn: true,
343 approvals: Vec::new(),
344 unknown_tagged_fields: UnknownTaggedFields::default(),
345 },
346 ],
347 unknown_tagged_fields: crate::krabka::test_support::sample_tagged_fields(),
348 }
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use assert2::assert;
355
356 use super::*;
357 use crate::krabka::test_support::{reject_truncated, roundtrip_case, unsupported_versions};
358
359 #[test]
360 fn requests_roundtrip() {
361 let cases = [
362 (
363 "default asks for every proposal",
364 DescribeBreakGlassRequest::default(),
365 ),
366 (
367 "one pending proposal",
368 DescribeBreakGlassRequest::populated(),
369 ),
370 (
371 "every pending proposal",
372 DescribeBreakGlassRequest {
373 pending_only: true,
374 proposal_id: Uuid::ZERO,
375 unknown_tagged_fields: UnknownTaggedFields::default(),
376 },
377 ),
378 ];
379 for (label, request) in &cases {
380 for version in MIN_VERSION..=MAX_VERSION {
381 roundtrip_case(label, request, version);
382 }
383 }
384 }
385
386 #[test]
387 fn responses_roundtrip() {
388 let cases = [
389 ("default", DescribeBreakGlassResponse::default()),
390 ("two proposals", DescribeBreakGlassResponse::populated()),
391 (
392 "refusal with no proposals",
393 DescribeBreakGlassResponse {
394 throttle_time_ms: 2,
395 error_code: 31,
396 error_message: Some("cluster authorization failed".to_string()),
397 proposals: Vec::new(),
398 unknown_tagged_fields: UnknownTaggedFields::default(),
399 },
400 ),
401 (
402 "proposal with no approvals",
403 DescribeBreakGlassResponse {
404 proposals: vec![DescribedBreakGlassProposal::default()],
405 ..DescribeBreakGlassResponse::default()
406 },
407 ),
408 ];
409 for (label, response) in &cases {
410 for version in MIN_VERSION..=MAX_VERSION {
411 roundtrip_case(label, response, version);
412 }
413 }
414 }
415
416 #[test]
417 fn a_nil_proposal_id_asks_for_every_proposal() {
418 assert!(
419 DescribeBreakGlassRequest::default()
420 == DescribeBreakGlassRequest {
421 pending_only: false,
422 proposal_id: Uuid::ZERO,
423 unknown_tagged_fields: UnknownTaggedFields::default(),
424 }
425 );
426 }
427
428 #[test]
429 fn an_unsigned_approval_carries_an_empty_signature() {
430 let response = DescribeBreakGlassResponse::populated();
431 let unsigned = &response.proposals[0].approvals[1];
432 assert!(unsigned.key_id.is_empty());
433 assert!(unsigned.signature.is_empty());
434 }
435
436 #[test]
437 fn truncated_input_is_rejected() {
438 reject_truncated(&DescribeBreakGlassRequest::populated(), MIN_VERSION);
439 reject_truncated(&DescribeBreakGlassResponse::populated(), MIN_VERSION);
440 }
441
442 #[test]
443 fn rejects_unsupported_versions() {
444 for version in unsupported_versions(MIN_VERSION, MAX_VERSION) {
445 let request = DescribeBreakGlassRequest::populated();
446 assert!(matches!(
447 request.encode(&mut Vec::<u8>::new(), version),
448 Err(ProtocolError::UnsupportedVersion {
449 api_key: API_KEY,
450 ..
451 })
452 ));
453 let response = DescribeBreakGlassResponse::populated();
454 assert!(matches!(
455 response.encode(&mut Vec::<u8>::new(), version),
456 Err(ProtocolError::UnsupportedVersion {
457 api_key: API_KEY,
458 ..
459 })
460 ));
461 }
462 }
463}