Skip to main content

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}