Skip to main content

parse_rust_server/
sessions.rs

1//! Session tokens.
2//!
3//! **In-memory, and that is a 0.1.0 limitation with consequences worth stating rather than
4//! discovering.** Upstream stores sessions in the `_Session` class, so they survive a restart and
5//! are visible to every node. This store does neither: restarting the server logs everyone out,
6//! and two parse-rust processes do not share sessions.
7//!
8//! That is acceptable for a proof of concept and unacceptable beyond it. It is recorded in
9//! the release notes rather than left implicit, because "sessions work" and "sessions
10//! work on one process until it restarts" look identical in a demo.
11//!
12//! The token *format* is not a shortcut: `r:` plus a 32-character random string is what upstream
13//! generates, and clients treat the prefix as meaningful.
14
15use std::collections::HashMap;
16use std::sync::RwLock;
17
18use parse_rust_core::object_id::random_string;
19
20/// Upstream's revocable-session prefix. A token without it is a legacy session.
21const REVOCABLE_PREFIX: &str = "r:";
22
23#[derive(Default)]
24pub struct SessionStore {
25    // token -> user objectId
26    by_token: RwLock<HashMap<String, String>>,
27}
28
29impl SessionStore {
30    /// Mint a session token for a user.
31    pub fn create(&self, user_object_id: &str) -> String {
32        let token = format!("{REVOCABLE_PREFIX}{}", random_string(32));
33        if let Ok(mut map) = self.by_token.write() {
34            map.insert(token.clone(), user_object_id.to_string());
35        }
36        token
37    }
38
39    /// Which user does this token belong to?
40    pub fn user_for(&self, token: &str) -> Option<String> {
41        self.by_token.read().ok()?.get(token).cloned()
42    }
43
44    /// Revoke one token. Returns whether it existed.
45    pub fn revoke(&self, token: &str) -> bool {
46        self.by_token
47            .write()
48            .map(|mut m| m.remove(token).is_some())
49            .unwrap_or(false)
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn tokens_carry_the_revocable_prefix() {
59        let s = SessionStore::default();
60        let t = s.create("u1");
61        assert!(
62            t.starts_with("r:"),
63            "clients treat the r: prefix as meaningful: {t}"
64        );
65        assert_eq!(t.len(), 2 + 32);
66    }
67
68    #[test]
69    fn a_token_resolves_to_its_user_and_only_its_user() {
70        let s = SessionStore::default();
71        let a = s.create("alice");
72        let b = s.create("bob");
73        assert_eq!(s.user_for(&a).as_deref(), Some("alice"));
74        assert_eq!(s.user_for(&b).as_deref(), Some("bob"));
75        assert_ne!(a, b);
76    }
77
78    #[test]
79    fn an_unknown_token_resolves_to_nobody() {
80        let s = SessionStore::default();
81        assert_eq!(s.user_for("r:nonsense"), None);
82        assert_eq!(s.user_for(""), None);
83    }
84
85    #[test]
86    fn revoking_removes_the_token() {
87        let s = SessionStore::default();
88        let t = s.create("u1");
89        assert!(s.revoke(&t));
90        assert_eq!(
91            s.user_for(&t),
92            None,
93            "a revoked token must resolve to nobody"
94        );
95        assert!(!s.revoke(&t), "revoking twice reports the second as a miss");
96    }
97}