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