pb_mapper_auth/ids.rs
1//! Identity types for temporary credentials.
2//!
3//! ```text
4//! KeyId (u64) — what a client presents
5//! ┌──────────────────────────┬──────────────────────────┐
6//! │ Generation (high 32) │ SlotIndex (low 32) │
7//! └──────────────────────────┴──────────────────────────┘
8//! which tenant of the row which row of the table
9//! ```
10//!
11//! These were all bare integers, which made `make_key_id(generation, slot)`
12//! accept its arguments in either order and let a slot index be compared against
13//! a generation without complaint. Separate types make both a compile error, and
14//! keep a `KeyId` from being used as an array index by mistake — the only way to
15//! get one is [`KeyId::slot`], which is also the only place the truncation to a
16//! row number is expressed.
17//!
18//! All three are `#[serde(transparent)]`, so persisted snapshots and the admin
19//! wire protocol keep the plain-integer encoding they already had.
20
21use std::fmt;
22
23use serde::{Deserialize, Serialize};
24
25/// The identity a client presents: a [`SlotIndex`] paired with the
26/// [`Generation`] of the row it was issued from.
27#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
28#[serde(transparent)]
29pub struct KeyId(u64);
30
31/// Which tenant of a slot a credential belongs to. Bumped every time the row is
32/// reissued, and never reset, so a retired credential can never match the row
33/// that replaced it.
34#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
35#[serde(transparent)]
36pub struct Generation(u32);
37
38/// Which row of the slot table a credential lives in.
39#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
40#[serde(transparent)]
41pub struct SlotIndex(u32);
42
43/// The administrator, which owns no slot and never expires.
44pub const ADMIN_KEY_ID: KeyId = KeyId(0);
45
46impl KeyId {
47 pub const fn new(generation: Generation, slot: SlotIndex) -> Self {
48 Self(((generation.0 as u64) << 32) | slot.0 as u64)
49 }
50
51 pub const fn generation(self) -> Generation {
52 Generation((self.0 >> 32) as u32)
53 }
54
55 pub const fn slot(self) -> SlotIndex {
56 SlotIndex(self.0 as u32)
57 }
58
59 pub const fn is_admin(self) -> bool {
60 self.0 == ADMIN_KEY_ID.0
61 }
62
63 /// The bytes mixed into the credential's key derivation.
64 pub const fn to_be_bytes(self) -> [u8; 8] {
65 self.0.to_be_bytes()
66 }
67
68 pub const fn as_u64(self) -> u64 {
69 self.0
70 }
71
72 pub const fn from_u64(raw: u64) -> Self {
73 Self(raw)
74 }
75}
76
77impl Generation {
78 pub const FIRST: Self = Self(0);
79
80 /// The generation for a reissue of this row, or `None` once the row has been
81 /// cycled `u32::MAX` times and can no longer produce a fresh identity.
82 pub fn next(self) -> Option<Self> {
83 self.0.checked_add(1).map(Self)
84 }
85
86 pub const fn as_u32(self) -> u32 {
87 self.0
88 }
89
90 pub const fn from_u32(raw: u32) -> Self {
91 Self(raw)
92 }
93}
94
95impl SlotIndex {
96 pub const fn as_index(self) -> usize {
97 self.0 as usize
98 }
99
100 /// # Panics
101 ///
102 /// If `index` exceeds `u32::MAX`. `MAX_TEMP_KEY_CAPACITY` caps the table far
103 /// below that, so a real index cannot reach it; panicking keeps a future
104 /// capacity change from silently wrapping into another row's identity.
105 pub fn from_index(index: usize) -> Self {
106 match u32::try_from(index) {
107 Ok(index) => Self(index),
108 Err(_) => panic!("slot index exceeds the addressable slot table"),
109 }
110 }
111}
112
113impl fmt::Display for KeyId {
114 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115 self.0.fmt(formatter)
116 }
117}
118
119impl fmt::Display for Generation {
120 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121 self.0.fmt(formatter)
122 }
123}
124
125impl fmt::Display for SlotIndex {
126 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127 self.0.fmt(formatter)
128 }
129}