Skip to main content

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}