krabka_metadata/break_glass.rs
1//! Break-glass proposal records, replicated through the raft quorum with
2//! `MetadataRecord::V1BreakGlassProposal` and
3//! `V1DeleteBreakGlassProposal`.
4//!
5//! A break-glass workflow needs two different people to agree before the
6//! broker does a privileged operation. An approved proposal is a standing
7//! authorization for a bounded window, and not a field on the request it
8//! authorizes, so no Kafka wire message changes. An operator gets the
9//! approval out of band, then runs the ordinary tool, and the broker reads
10//! the proposal out of the metadata image.
11//!
12//! The records stay pure data. The approver set, the distinct-principal rule,
13//! and the signature checks live in `krabka-broker`. The one rule this crate
14//! enforces is in
15//! [`MetadataImage::validate`](crate::MetadataImage::validate): an incoming
16//! approval list must extend the stored one, so two concurrent approvals
17//! cannot overwrite each other.
18
19use serde::{Deserialize, Serialize};
20use uuid::Uuid;
21
22/// The privileged transitions that a break-glass proposal can authorize.
23///
24/// Each one can lose committed data, or can lift the protection that stops
25/// another one from losing committed data.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27pub enum BreakGlassAction {
28 /// Remove a write-freeze registry entry.
29 ThawTopicFreeze,
30 /// Elect a leader that is not in the ISR.
31 UncleanElectLeaders,
32 /// Recover a partition whose whole ISR is gone.
33 UncleanRecovery,
34 /// Drop the registration of a broker.
35 UnregisterBroker,
36 /// Cancel a partition reassignment that is in flight.
37 CancelReassignment,
38 /// Delete a topic.
39 DeleteTopic,
40 /// Delete records below an offset.
41 DeleteRecords,
42}
43
44/// One approval on a proposal.
45///
46/// The broker checks that the principal is in the configured approver set,
47/// that it is not the proposer, and that it is not already in the list.
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
49pub struct BreakGlassApproval {
50 /// The approver, in `KafkaPrincipal` string form, for example
51 /// `"User:bob"`.
52 pub principal: String,
53 /// Epoch milliseconds at which the approval was made.
54 pub approved_at_ms: i64,
55 /// The operator key that signed the approval. It is empty when the
56 /// approval is unsigned.
57 pub key_id: String,
58 /// Detached Ed25519 signature over the canonical bytes of the proposal.
59 /// It is empty when the approval is unsigned. This crate keeps the bytes
60 /// opaque; `krabka-broker` builds and verifies them.
61 pub signature: Vec<u8>,
62}
63
64/// One break-glass proposal, keyed by [`Self::proposal_id`].
65///
66/// The record is authoritative for that id: a later record with the same id
67/// replaces the earlier one, which is how an approval and a consumption are
68/// both written. `V1DeleteBreakGlassProposal` removes the entry, and the
69/// expiry sweep emits it.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
71pub struct BreakGlassProposalRecord {
72 pub proposal_id: Uuid,
73 pub action: BreakGlassAction,
74 /// What the action applies to. The shape depends on the action: a
75 /// `"<pattern>:<scope>"` pair, a `"<topic>-<partition>"` pair, a topic
76 /// name, or a broker id.
77 pub target: String,
78 /// The principal that proposed the action, in `KafkaPrincipal` string
79 /// form. It cannot also approve.
80 pub proposer: String,
81 /// Free text that says why the action is needed. It goes to the audit
82 /// log.
83 pub reason: String,
84 pub created_at_ms: i64,
85 /// Epoch milliseconds after which the proposal no longer authorizes
86 /// anything. It bounds how long a stale approval stays usable.
87 pub expires_at_ms: i64,
88 /// The approvals collected so far, in the order they were made. A record
89 /// can only append to this list. See
90 /// [`MetadataImage::validate`](crate::MetadataImage::validate).
91 pub approvals: Vec<BreakGlassApproval>,
92 /// Epoch milliseconds at which the approval was spent on the transition
93 /// it authorized. `0` means unconsumed. The gated handler stamps it and
94 /// puts the stamped record in the same raft append as the transition, so
95 /// the two commit together.
96 pub consumed_at_ms: i64,
97 /// `true` when the proposer withdrew the proposal. A withdrawn proposal
98 /// authorizes nothing.
99 pub withdrawn: bool,
100}