Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/edda

crates/nornir-identity-session/src/session.rs

//! **Session revocation floors** (moved from Holger's `passkey.rs`, A34 M-5).
//!
//! Floors under a session's `auth_time`, never a second epoch: a verifier built
//! with a fixed epoch needs no change to the token. `all_before` is the hammer
//! (revoke every session); `subjects` is one person's logout (every device they
//! hold), and the revocation a credential's removal implies.

use std::collections::BTreeMap;

use serde::{Deserialize, Serialize};

/// The floors, as a product persists them.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Revocations {
    /// Every session minted at or before this second is void (0: none).
    #[serde(default)]
    pub all_before: i64,
    /// Per subject: every session of theirs minted at or before this second is void.
    #[serde(default)]
    pub subjects: BTreeMap<String, i64>,
}

impl Revocations {
    /// Is a session minted at `auth_time` for `subject` still allowed?
    ///
    /// STRICTLY above a floor: a floor is a whole second, and a session minted
    /// in the same second as a logout may have been minted a moment BEFORE it,
    /// the one that must not survive. The cost is the other direction: a login
    /// in the very second of a revocation is refused once and works a second
    /// later.
    pub fn admits(&self, subject: &str, auth_time: i64) -> bool {
        (self.all_before == 0 || auth_time > self.all_before)
            && self.subjects.get(subject).is_none_or(|floor| auth_time > *floor)
    }

    /// **Log a person out of every device**: a floor under their sessions at
    /// `now`. A floor never moves back.
    pub fn revoke_subject(&mut self, subject: &str, now: i64) {
        let floor = self.subjects.entry(subject.to_owned()).or_insert(now);
        *floor = (*floor).max(now);
    }

    /// **Log everyone out**: a floor under every session at `now`.
    pub fn revoke_all(&mut self, now: i64) {
        self.all_before = self.all_before.max(now);
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_floor_voids_what_was_minted_up_to_it_and_nothing_after() {
        let mut r = Revocations::default();
        assert!(r.admits("kim", 1), "no floor: everything is admitted");
        r.revoke_subject("kim", 100);
        assert!(!r.admits("kim", 99));
        assert!(!r.admits("kim", 100), "the same second may be before the logout");
        assert!(r.admits("kim", 101));
        assert!(r.admits("bo", 50), "another person is untouched");
        r.revoke_subject("kim", 10);
        assert!(!r.admits("kim", 100), "a floor never moves back");
        r.revoke_all(200);
        assert!(!r.admits("bo", 200) && r.admits("bo", 201));
        r.revoke_all(150);
        assert_eq!(r.all_before, 200);
    }

    #[test]
    fn the_floors_read_back_as_written_and_an_old_file_reads_as_none() {
        let mut r = Revocations::default();
        r.revoke_subject("kim", 7);
        r.revoke_all(3);
        let json = serde_json::to_string(&r).unwrap();
        assert_eq!(serde_json::from_str::<Revocations>(&json).unwrap(), r);
        assert_eq!(serde_json::from_str::<Revocations>("{}").unwrap(), Revocations::default());
    }
}