featherbit/stores/namespaces.rs
1//! The key namespaces featherbit writes under a store's `key_prefix`.
2//!
3//! Every key is `{key_prefix}:{namespace}:...`. These must stay disjoint: a
4//! policy-written key must never be able to name a key a managed subsystem
5//! reads or writes, in either direction. Declaring them in one place -- and
6//! testing the real builders against it -- is what keeps that true as
7//! subsystems are added.
8//!
9//! The whole module is gated on `redis-store`: every subsystem it describes is,
10//! so in a headless build there are no keys to namespace.
11
12/// Rate-limit counters (`src/stores/counter.rs`).
13pub const COUNTERS: &str = "cnt";
14/// ACME account, certificate, challenge and lease state (`src/acme/storage/redis.rs`).
15pub const ACME: &str = "acme";
16/// Server-side sessions (`src/sessions/redis.rs`).
17pub const SESSIONS: &str = "sess";
18/// Server-side session refresh locks (`src/sessions/redis.rs`'s `lock_key`).
19pub const SESSION_LOCKS: &str = "lock";
20/// Subject -> session-id index, for revoke-by-subject (`src/sessions/redis.rs`'s `subj_key`).
21pub const SESSION_SUBJECTS: &str = "subj";
22
23/// Policy-written keys: the `store-get`/`store-set`/`store-incr`/`store-delete` nodes.
24pub const POLICY_KV: &str = "kv";
25
26/// Cached responses (`proxy-cache` with `policy: redis`).
27pub const CACHE: &str = "cache";
28
29/// Namespaces owned by featherbit itself. A policy can never address these,
30/// because every `store-*` key is prefixed with [`POLICY_KV`].
31///
32/// Test-only by design: nothing in production consults this list. It exists so
33/// the disjointness it describes is asserted rather than assumed.
34#[cfg(test)]
35pub const MANAGED: &[&str] = &[
36 COUNTERS,
37 ACME,
38 SESSIONS,
39 SESSION_LOCKS,
40 SESSION_SUBJECTS,
41 CACHE,
42];
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 /// A policy must never be able to address a namespace featherbit owns.
49 #[test]
50 fn test_policy_namespace_is_not_managed() {
51 assert!(!MANAGED.contains(&POLICY_KV));
52 }
53
54 #[test]
55 fn test_all_namespaces_are_distinct() {
56 let all = [
57 COUNTERS,
58 ACME,
59 SESSIONS,
60 SESSION_LOCKS,
61 SESSION_SUBJECTS,
62 CACHE,
63 ];
64 for (i, a) in all.iter().enumerate() {
65 for b in all.iter().skip(i + 1) {
66 assert_ne!(a, b, "namespaces must be pairwise distinct");
67 }
68 }
69 }
70
71 /// The drift guard: each subsystem's real key builder must still produce
72 /// keys under its declared namespace. A subsystem that changes its prefix,
73 /// or a new one that reuses `kv`, fails here.
74 #[test]
75 fn test_key_builders_stay_inside_their_declared_namespace() {
76 let sess = crate::sessions::redis::sess_key("fb", "abc");
77 assert!(sess.starts_with(&format!("fb:{}:", SESSIONS)), "{sess}");
78
79 let acme = crate::acme::storage::redis::account_key("fb");
80 assert!(acme.starts_with(&format!("fb:{}:", ACME)), "{acme}");
81
82 let cnt = crate::stores::counter::window_key("fb", 7, "u1");
83 assert!(cnt.starts_with(&format!("fb:{}:", COUNTERS)), "{cnt}");
84
85 let lock = crate::sessions::redis::lock_key("fb", "abc");
86 assert!(
87 lock.starts_with(&format!("fb:{}:", SESSION_LOCKS)),
88 "{lock}"
89 );
90
91 let subj = crate::sessions::redis::subj_key("fb", "alice");
92 assert!(
93 subj.starts_with(&format!("fb:{}:", SESSION_SUBJECTS)),
94 "{subj}"
95 );
96
97 let cache = crate::stores::redis_cache::cache_key("fb", "abc");
98 assert!(cache.starts_with(&format!("fb:{}:", CACHE)), "{cache}");
99 }
100}